0%

IdentityServer4 筆記

identityserver4

因為接了個燙手山芋 , 要把 .net framework 升級到 .net 6 , 陳年的包袱不好搞 , 還要可以串 SSO , 所以筆記下
以前頂多就是串串 line login 自己內部網站要搞 SSO 還真沒頭緒 , 碰巧遇到 保哥有開課 就自己丟錢去上
不過他這個課很講概念 , 後續沒自己實作一定陣亡 XD
如果不想花錢的話可以看 這個大陸 MVP 教學
他的 code 在此

我後來有做個 FineReport 串接 SSO 的 lab 有興趣也可以參考看看

Skoruba identityserver4 Admin

純 IdentityServer4 是沒有 UI 去管理你的 Client , 如果要的話好像要付錢 , 所以有大神搞了個免錢專案
老實說完全沒接觸過的話不太可能搞得起來 OAuth 2.0 實在複雜到暈頭轉向 , 廢話不多說實作先
他的文件 在此

1
git clone https://github.com/skoruba/IdentityServer4.Admin.git

clone 下來以後先在 Solution => Properties => Common Properties => Startup Project => Multiple startup projects 選單內
設定以下三個 Skoruba.IdentityServer4.Admin Skoruba.IdentityServer4.Admin.Api Skoruba.IdentityServer4.STS.Identity 為 Start 就能動了

Skoruba.IdentityServer4.Admin

https://localhost:44303/

這個做用是後臺 GUI , 也就是純 IdentityServer4 沒給你的東西 , 用這個可以管理你的 Client
他的帳號密碼是 admin Pa$$word123 其他詳細訊息可以在 identitydata.json or identityserverdata.json 這兩隻檔案找到

在 OAuth2.0 裡面 Client 可以看做 console app or web app 這類東西 , 類似 line api 後臺管理介面的概念
這個網站本身就是一個 Client 他也受到 IdentityServer4.STS.Identity 他進行保護 , 啟動後你的 localdb 會多一堆 table 之後再研究
點選 https://localhost:44303/Configuration/Clients 進去以後可以看到預設有兩個 client skoruba_identity_admin_api_swaggerui skoruba_identity_admin
接著點 skoruba_identity_admin => Edit => Basic 可以看到這個就是走 authorization_code 這個流程
另外有幾個重要的網址要記下 , 後續設定自己的 mvc 網站就要模仿這樣設定 , 如果近來看到中文的話右下角可以改成英文 , 不然翻譯的很鬼畜

Redirect Uri => https://localhost:44303/signin-oidc
Front Channel Logout Uri => https://localhost:44303/signout-oidc
Post Logout Redirect Uris => https://localhost:44303/signout-callback-oidc
Allowed Cors Origins => https://localhost:44303
Client Uri => https://localhost:44303

Skoruba.IdentityServer4.Admin.Api

https://localhost:44302/swagger/index.html

這個是他給的 swagger , 實作上暫時沒遇到要直接呼叫他

Skoruba.IdentityServer4.STS.Identity

https://localhost:44310/

這個等價於 IdentityServer4 的 STS (security token service) , 當你要設定你自己的網站受到保護時 , 要指向他 , 千萬不要設定錯了
剛開始不小心寫成 https://localhost:44303/ 所以狂噴 Error
點選 Discovery Document 會跳到這個網址 https://localhost:44310/.well-known/openid-configuration
如果有在自己的 c# 程式上安裝 IdentityModel 這個套件的話他會自己去撈並且轉成類別 , 馬上收工
另外如果要讓登出以後回到自己的網站 , 可以改這個類別內的 AutomaticRedirectAfterSignOuttrue 即可登出後回到自己網站

1
2
3
4
5
6
7
8
9
10
11
public class AccountOptions
{
public static bool AllowLocalLogin = true;
public static bool AllowRememberLogin = true;
public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);

public static bool ShowLogoutPrompt = true;
public static bool AutomaticRedirectAfterSignOut = true;

public static string InvalidCredentialsErrorMessage = "Invalid username or password";
}

實作 Client Credentials

這個比較簡單會用這個表示 機器 他不代表任何人 , 所以要走這個流程 , 說穿就是諸如 console 排程這類東西
這裡有個細節 , 如果你的 scope 寫 email openid 之類的會噴 error , 要記得先去新增 Api 自己的 scope

Identity Server 設定

先到 https://localhost:44303/ => Clients/Resources => Api Scopes
Add Api Scope => Name => DemoApiClientCredentials => Save Api Scope
接著回到 https://localhost:44303/
Add Client => Machine/Robot Client Credentials flow
ClientId => DemoApiClientCredentials
Allowed Scopes => DemoApiClientCredentials
Allowed Grant Types => client_credentials
Client Name => DemoApiClientCredentials => Save Client
接著點 Manage Client Secrets
Secret Value => DemoApiClientCredentials
Description => DemoApiClientCredentials
Add Client Secret
另外注意到 Access Token Lifetime 預設 token 有效期限為 3600 / 60 = 1hr 視情況調整

程式碼修正

首先安裝 Microsoft.AspNetCore.Authentication.JwtBearer 注意下版本 6.0.16

調整 Program , 多加一組給 Jwt 的設定在前面 , 最後 postman 呼叫時噴了個莫名其妙的錯誤 the audience is invalid , 可以參考這篇處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:44310/";
options.RequireHttpsMetadata = false;
options.Audience = "DemoApiClientCredentials";

//the audience is invalid
//https://jscinin.medium.com/asp-net-core-3-1-%E7%B6%93jwt%E8%AA%8D%E8%AD%89%E5%BE%8C-post-api-%E9%8C%AF%E8%AA%A4%E8%A8%8A%E6%81%AF401-the-audience-is-invalid-39540ebe4b6e
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = false,
ValidateAudience = false,
};
});

最後在 Controller 上面加入 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 屬性即可

Postman 呼叫受保護的 Api

https://identityserver4.readthedocs.io/en/latest/endpoints/token.html
https://localhost:44310/.well-known/openid-configuration

method => post
網址 => https://localhost:44310/connect/token
content type => x-www-form-urlencoded
client_id => DemoApiClientCredentials
client_secret => DemoApiClientCredentials
grant_type => client_credentials
scope => DemoApiClientCredentials

打出去後就得到下列結果

1
2
3
4
5
6
{
"access_token": "eyJhbGciOiJSUzoxmtpZCI6000xxxEQTFFRURDNjNDQkVDNDY4N0Q5MzdDNThCM0ZBQjYxIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2ODEzNTczMDAsImV4cCI6MTY4MTM2MDkwMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMTAiLCJjbGllbnRfaWQiOiJEZW1vQXBpQ2xpZW50IiwiaWF0IjoxNjgxMzU3MzAwLCJzY29wZSI6WyJEZW1vQXBpQ2xpZW50Il19.CTV-fHoW9MYDMoPHGr5Xc9F1-9OJxLT84w18kdxZcFAr9BmtGW0LjYrAfZbnZCe6J1N_Wi5KsSHzsobeZc9MO6EhzFEe_48KsdM-qmYvY2NeQHMq0DM3RtHEflrN2lnICas0Vdv7HKGC3SLV0AipOAor6SSU8G3c_MDjZglQ-rz3rP-FwWVxWf1haJY2ZFEJYmdKuElNX2fbzpZRkiiSB82Bmudtec284hFfP1Wu4GLzY-3P6TKtbSyB0eH2YXHIgcqIjX5ZcIYCdMYmBlsngU34QHLgV-hD6m_3CNJFiKeR-jjBGxpZRYMZRYofxrld5iJp8lhQbbLzpwOTh6FmYw",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "DemoApiClientCredentials"
}

接著用 postman 呼叫你的 api

method => post
網址 => https://localhost:3001/LaSai/sais
Headers => Authorization => Bearer token 特別注意要 Bearer 後面有空白格 XD

1
Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IxxooxxxRURDNjNDQkVDNDY4N0Q5MzdDNThCM0ZBQjYxIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2ODEzNTgzOTAsImV4cCI6MTY4MTM2MTk5MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMTAiLCJjbGllbnRfaWQiOiJEZW1vQXBpQ2xpZW50IiwiaWF0IjoxNjgxMzU4MzkwLCJzY29wZSI6WyJEZW1vQXBpQ2xpZW50Il19.nOO6CG8AkVZ6PAzsD5r4unmDK3wrnLBHCqhsjz0mmO_ZXL-NsKdTWPHPhDKKKhz9IXQ4CEKkKbPuxCwICO9TCjtWakLdB1S86NEqhXgeQ_i3BlYJYQyfMV5F1NgoTAOFUhdE1XdPiFjmE1YB8MAP3Db_LZ3jD940IcAVzFpEfnIG01qojvhjEpxlzo5LYc8hxaPHgDSRl9VyWrKzxV3iPCLWnuWRrVo6irHrpaJcbTaukjU9ZBkIJKdtdyJzi7E1Vl0BSmjAZhaXkJB0m10o4pEdOre4SmlXFaEHO6COt9Rl_LFC7bzAvNTujt7NOSpoDMQp3FlWQp4KIf3I6nAJcw

實作 Authorization Code

實作

這個應該都是大多數人要搞 SSO 會用的 , 反正就是 mvc 站台拿 token 然後做事
先開一個 .net 6 mvc 專案 , 接著下載這個套件 Microsoft.AspNetCore.Authentication.OpenIdConnect
然後開啟 launchSettings.json 找到你的網址

1
2
3
4
5
6
7
8
9
"Net6SSOMVC": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7069;http://localhost:5069",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},

接著到 Skoruba identityserver4 Admin 後臺 https://localhost:44303 去新增一個 Client
ClientId => mvc
Client Name => mvc
Client Secrets => mvc
然後選 Web Application - Server side Authorization Code Flow with PKCE => Save
Allowed Scopes 這裡先把有的選項都加進去
Redirect Uris => https://localhost:7069/signin-oidc 注意這裡要設定你自己站台的網址
Allowed Grant Types => authorization_code 這個要留意下 , 我忘了加 XD 然後狂噴 Error
Front Channel Logout Uri => https://localhost:7069/signout-oidc
Post Logout Redirect Uris => https://localhost:7069/signout-callback-oidc
Allowed Cors Origins => https://localhost:7069 應該沒設定也沒關係 暫無研究
Client Uri => https://localhost:7069
Require Consent => 這個設定 false 的話其實有點強姦的味道 , 就是直接同意啦 (這個在企業內部應該表示資料是屬於公司的)
如果設定 true 的話就會跳出來你要允許那些的選項
Logo Uri => 這個用在同意頁面 , 有設定的話上面會有 logo 可是我測的機器上好像沒開 CORS 所以爆炸 https://localhost:7069/images/logo.png
Always Include User Claims In IdToken 這個選項有 enable 的話就會自動把你的 Claim 加上去 , 少寫一些 code
最後 Save

接著 Program 改成這樣 , 比較特別的就是 Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; 這句
debug 過程中如果沒設定這樣的話 , 他會把詳細訊息吃掉 , 最好加上 , 不然那個訊息看不太懂
另外就是 Authority 這個要注意 , 不要寫成 https://localhost:44303

如果寫錯會炸這樣 (有加上 Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true)

1
InvalidOperationException: IDX20803: Unable to obtain configuration from: 'https://localhost:44303/.well-known/openid-configuration'.

如果寫錯會炸這樣 (沒加上 Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII) 會吃案 , 噴一堆看不懂的

1
IOException: IDX20807: Unable to retrieve document from: 'System.String'. HttpResponseMessage: 'System.Net.Http.HttpResponseMessage', HttpResponseMessage.Content: 'System.String'.

另外注意 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 這句如果不設定的話就會以網址形式呈現 , 加上去才會以 well-known 形式呈現

Program

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.IdentityModel.Tokens.Jwt;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

//這句如果不設定的話就會以網址形式呈現 , 加上去才會以 well-known 形式呈現
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

//加入詳細訊息
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;


builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:44310/";
options.ClientId = "mvc";
options.ClientSecret = "mvc";
options.ResponseType = "code";

//options.Scope.Add("openid");
//options.Scope.Add("profile");
options.SaveTokens = true;

//這裡可以用來把以前 .net framework 內 UseOpenIdConnectAuthentication
//Notifications = new OpenIdConnectAuthenticationNotifications { ... }
//相關 Event 加上去 https://dotblogs.com.tw/anyun/2021/04/25/173529
options.Events = new OpenIdConnectEvents
{

};


});


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}



app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");


app.Run();

後來發現之前舊版有插 log 所以補下 , 補在 builder.Services.AddAuthentication 前即可 , .net6 真是太精簡了 .. 都不太曉得要怎麼寫 XD

1
2
3
4
5
6
7
8
//以工廠建立 log
using var loggerFactory = LoggerFactory.Create(x =>
{
x.AddConsole();
});

var logger = loggerFactory.CreateLogger<Program>();
builder.Services.AddAuthentication(options => ...

另外要加工自己的一些 Claim 可以參考這篇 , 舊版這個叫做 SecurityTokenValidated , .net6 裡面叫做 OnTokenValidated
礙於之前沒玩過 , 反正一個 Scope 底下可以有很多的 Claim , 這部分可以自己去設定看權限要怎樣設計
此外要拿到 Code , AccessToken , RefreshToken 等等重要資訊則可以依靠 TokenEndpointResponse 這咚咚
我看舊版的在 ProtocolMessage 這裡面有寫東西 , 可是 .net6 我撈裡面的子屬性都 null

1
2
3
4
OnTokenValidated = async notification => {
var accessToken = context.TokenEndpointResponse.AccessToken;
//... 看你還要拿啥
}

接著讓 HomeController 內受到保護 , 把需要保護的 Action 加上這段 [Authorize] 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Net6SSOMVC.Models;
using System.Diagnostics;

namespace Net6SSOMVC.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

public async Task LogOut()
{
//先登出自己的站台
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

//登出 STS
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}

//或這樣寫
//public IActionResult Logout()
//{
// return SignOut("Cookies", "oidc");
//}


[Authorize]
public async Task<IActionResult> Privacy()
{
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);

ViewData["accessToken"] = accessToken;
ViewData["idToken"] = idToken;
ViewData["refreshToken"] = refreshToken;

return View();
}


[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

然後修改 _Layout.cshtml 加入登出功能

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="LogOut">LogOut</a>
</li>
</ul>
</div>

至此就可以測看看 , 記得先從 https://localhost:44310/ 上面登出 , 這樣才可以看到跳進去頁面的效果 後續應該就是自己串自己的 DB , 有空再寫

jquery 或 前後端混合的問題

一開始有點想不通 , 如果純前端用 Implicit 那混合前後端要用啥 , 後來想想應該依靠後端去走 Authorization Code Flow
下面這個 lab 如果你用 jquery 要呼叫受保護的 api 也是要先登入不然會噴 CORS 的 error
TestController.cs

1
2
3
4
5
6
7
8
[Authorize]
public class TestController : ControllerBase
{
public string HelloWorld()
{
return "HelloWorld";
}
}

所以要把 Authorize 擋在 Controller or Action 上
HomeController.cs

1
2
3
4
5
[Authorize]
public IActionResult JQ()
{
return View();
}

JQ.cshtml

1
2
3
4
5
6
7
8
9
10
11
<h2>JQuery</h2>


@section Scripts{
<script>
console.log('test jq');
$.get('https://localhost:7069/Test/HelloWorld', function(data, status){
console.log('Data: ' + data + 'Status:' + status);
});
</script>
}

沒擋的話會噴這樣

1
2
3
4
5
Access to XMLHttpRequest at 'https://xxx.com/sts/connect/authorize?client_id=net6mvc&redirect_uri=....'
(redirected from 'https://localhost:7069/Test/HelloWorld') from
origin 'https://localhost:7069' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
補 CORS
1
2
3
4
5
6
7
8
9
//加入允許 CORS
builder.Services.AddCors(p => p.AddPolicy("CORS", builder =>
{
builder.WithOrigins("*").AllowAnyMethod().AllowAnyHeader();
}));


//設定允許 CORS
app.UseCors("CORS");

Angular 實作 Authorization Code

主要是靠這個angular-auth-oidc-client 套件
另外他還有提供大量範例 可以參考看看

前端最主要的設定如下
authority => 表示你的 security token service , 所以應該會是 https://localhost:44310
redirectUrl => 表示你這個 angular 站台的網址 , 懶得寫的話就直接用 window.location.origin
postLogoutRedirectUri => 登出後回來的頁面 , 懶得寫也是直接用 window.location.origin
這裡實作上有遇到很雷的部分 , 一般啟動 angular 預設的 port 都是 4200 , 測試的時候都會用自己 ip 去測試
可是 sso server 因為已經上線 , 並且還針對網域去限定 , 所以導致我狂噴 CORS , 而且也是使用 https 所以條件嚴苛
這時候可以先用 ipconfig /all 來查自己電腦在內網叫啥
所以你的網址應該長這樣 https://hahaha.lasai.com , 這時候 redirectUrl 實際上是 https://hahaha.lasai.com

另外還遇到忘了使用 https 噴的錯誤 , 如果噴這句的話應該是沒設定 https , 要確保自己的路徑都走 https
main.c7f7bb43c5958d73.js:1 ERROR TypeError: Cannot read property 'digest' of undefined

1
2
3
4
5
6
7
8
9
10
ipconfig /all

Windows IP 設定

主機名稱 . . . . . . . . . . . . .: hahaha
主要 DNS 尾碼 . . . . . . . . . .: lasai.com
節點類型 . . . . . . . . . . . . .: 混合式
IP 路由啟用 . . . . . . . . . . . : 否
WINS Proxy 啟用 . . . . . . . . . : 否
DNS 尾碼搜尋清單 . . . . . . . . .: lasai.com

接著要在 package.jsonscripts 進行調整 , 追加 ng serve --ssl --host 0.0.0.0 --disable-host-check --port 443 就可以讓 angular 以 https 的方式啟動

1
2
3
4
5
6
7
8
9
"scripts": {
"ng": "ng",
"start": "ng serve",
"starthost": "ng serve --ssl --host 0.0.0.0 --disable-host-check",
"startmachine": "ng serve --ssl --host 0.0.0.0 --disable-host-check --port 443",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
AuthModule.forRoot({
config: {
authority: 'https://localhost:44310',
redirectUrl: 'https://hahaha.lasai.com',
postLogoutRedirectUri: window.location.origin,
clientId: 'angular',
scope: 'openid profile email offline_access',
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.Debug,
},
}),

還有個雷點 , 預設情況下當你開新分頁的話就需要重新登入 , 所以要在 providers 追加這個部分 , 就可以用 localStorage 去存

1
2
3
4
5
6
7
8
9
10
11
12
providers:[
{
/**
* https://angular-auth-oidc-client.com/docs/documentation/configuration
* 這裡預設每次開新的視窗就會被清除登入 , 所以要設定以下這兩個 config 才可以 keep 住
* Using localstorage instead of default sessionstorage
* The angular-auth-oidc-client uses session storage by default that gets cleared whenever you open the website in a new tab, if you want to change it to localstorage then need to provide a different AbstractSecurityStorage.
*/
provide: AbstractSecurityStorage,
useClass: DefaultLocalStorageService,
}
]

最後我測試他好像不會自動判斷你的 idToken 是否已經到期 , 當 token 時間到以後你還是處於登入狀態 , 所以要點 f5 刷了才是登出
因為還在摸索中 , 我先在 app.component 的 ngOnInit 函數裡面加上 setInterval , 我看印度人是用 setTimeout 應該都類似

1
2
3
4
5
6
7
8
9
10
//當時間到了自動刷新登出
setInterval(() => {
console.log('check idToken Expired')
if (this.idToken) {
console.log('isTokenExpired', this.jwtHelper.isTokenExpired(this.idToken))
if (this.jwtHelper.isTokenExpired(this.idToken) === true) {
this.oidcSecurityService.logoff().subscribe((result) => console.log(result));
}
}
}, 1000 * 10)

另外 web api 有保護的話 , 可以用 HttpInterceptor 去加上 jwt token 類似這樣吧!? 還在摸索階段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export class BasicAuthHttpInterceptor implements HttpInterceptor {

constructor(public oidcSecurityService: OidcSecurityService) { }

intercept(req: HttpRequest<any>, next: HttpHandler) {
//直接從 localstorage 去拿
// if (req.url.startsWith('https://123.45.67.89:3001')) {
// let data = JSON.parse(localStorage.getItem('0-angular')!)
// if (data) {
// let token = data['authnResult']?.['access_token']
// if (token) {
// req = req.clone({
// setHeaders: { Authorization: `Bearer ${token}` }
// });

// //如果有拿到的話
// return next.handle(req);
// }
// }
// }

//不太確定會不會有其他問題 , 應該暫時可以
if (req.url.startsWith('https://123.45.67.89:3001')) {
this.oidcSecurityService.checkAuth().subscribe(({ accessToken }) => {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
}
});
return next.handle(req);
})
}


return next.handle(req);

}
}

IdentityServer 設定

Name
ClientId => angular
Client Name => angular

Basics
Allow Offline Access => 打勾
Allow Access Token Via Browser => 打勾
Allowed Scopes => openid profile email
Redirect Uris => https://hahaha.lasai.com
Allowed Grant Types => authorization_code

Authentication/Logout
Post Logout Redirect Uris => https://hahaha.lasai.com

Token
Identity Token Lifetime => 86400 = 60秒 * 60分鐘 * 24小時 這個預設只有 300 秒 5 分鐘的樣子 , 登出的頻率反映真實世界的登出頻率 XD

Implicit 實作

理論上來說會用這個實際上應該是 SPA 也就是純前端 Angular Vue React 這類 , 不過工作環境大多是混合前後 , 所以有空再寫詳細 XD
工作上遇到舊版的 code 也是走 Implicit 整個怪怪低
好像要在 UI 上勾選 Allow Access Token Via Browser
Program.cs

1
2
3
4
5
//...

.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
//implicit
options.ResponseType = "id_token token";

postgresql 16

這是我在 postgresql 16 遇到的問題 , 我的解法是 , 因為朋友用新專案 , 所以直接上 Duende.IdentityServer.Admin XD
InvalidCastException: Cannot write DateTime with Kind=Local to PostgreSQL type ‘timestamp with time zone’, only UTC is supported.
Note that it’s not possible to mix DateTimes with different Kinds in an array/range. See the Npgsql.EnableLegacyTimestampBehavior AppContext switch to enable legacy behavior.

不過他新的文件不曉得是不是我沒找到怎麼設定的章節 , 我怎麼覺得舊版 這段 用 postgresql 寫得比較清楚

IdentityServer4

https://identityserver4.readthedocs.io/en/latest/quickstarts/0_overview.html#preparation
首先先安裝範本

1
2
3
4
5
6
7
8
9
10
11
dotnet new -i IdentityServer4.Templates

#查看目前範本
dotnet new -l

IdentityServer4 Empty is4empty [C#] Web/IdentityServer4
IdentityServer4 Quickstart UI (UI assets only) is4ui [C#] Web/IdentityServer4
IdentityServer4 with AdminUI is4admin [C#] Web/IdentityServer4
IdentityServer4 with ASP.NET Core Identity is4aspid [C#] Web/IdentityServer4
IdentityServer4 with Entity Framework Stores is4ef [C#] Web/IdentityServer4
IdentityServer4 with In-Memory Stores and Test Users is4inmem [C#] Web/IdentityServer4

然後看到一個很懶的語法 md , 其實就是 mkdir .. 有必要這麼懶嗎 XD

1
2
3
4
5
6
7
md quickstart
cd quickstart

md src
cd src

dotnet new is4empty -n IdentityServer

無限 loop 與找不到 signin-oidc 的天坑

在舊版 asp.net mvc 5 裡面用 SSO 的話 , 好像都會踩到這個雷 , 可是在 .net core 什麼都不用加直接秒殺
不曉得到底是哪個環節有問題 , 我下面這個設定方法可以運氣好避開 loop 詳細什麼原理我也不曉得 , 可能都要設定 https?
看網路上有些資源說是 chrome cookie SameSite 有問題或是要你安裝 owin-cookie-saver
不過好像也都不見得能解 , 反正就多比拚運氣吧… 試不過也別打我 XD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
  public void ConfigureAuthNoLoop( IAppBuilder app )
{
//app.UseKentorOwinCookieSaver();
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

//這句放在 Global 裡面
//AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

// 清除掉預設的Claim Type對應
//JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseCookieAuthentication( new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
//CookieSameSite = SameSiteMode.None,
} );

app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44310",
ClientId = "mvc",
ClientSecret = "mvc",
RedirectUri = "https://localhost:44356/signin-oidc",//Net4MvcClient's URL

PostLogoutRedirectUri = "https://localhost:44356",
ResponseType = "id_token token",
RequireHttpsMetadata = false,

Scope = "openid profile email",

TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name"
},

SignInAsAuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,

Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
n.AuthenticationTicket.Identity.AddClaim( new Claim( "access_token", n.ProtocolMessage.AccessToken ) );
n.AuthenticationTicket.Identity.AddClaim( new Claim( "id_token", n.ProtocolMessage.IdToken ) );

var discoveryDocument = new DiscoveryDocumentRequest()
{
Address = "https://localhost:44310",
Policy = {
RequireHttps = true,
Authority = authority,
ValidateEndpoints = true
},
};

var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync( discoveryDocument );
if( disco.IsError ) return;

// 建立取得id token的需求物件
var idTokenRequest = new UserInfoRequest
{
Address = disco.UserInfoEndpoint,
Token = n.ProtocolMessage.AccessToken
};

//建立取得id token的endPoint連接,傳入access token作為參數後,取回使用者資訊
var userInfoResponse = await client.GetUserInfoAsync( idTokenRequest );
n.AuthenticationTicket.Identity.AddClaims( userInfoResponse.Claims );

n.OwinContext.Authentication.User.AddIdentity( n.AuthenticationTicket.Identity );
//這句好像可有可無
System.Web.HttpContext.Current.User = n.OwinContext.Authentication.User;

n.OwinContext.Authentication.SignIn(n.AuthenticationTicket.Identity);
},
RedirectToIdentityProvider = n =>
{
if( n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout )
{
var id_token_claim = n.OwinContext.Authentication.User.Claims.FirstOrDefault( x => x.Type == "id_token" );
if( id_token_claim != null )
{
n.ProtocolMessage.IdTokenHint = id_token_claim.Value;
}
}
return Task.FromResult( 0 );
},
}
} );
}

asp.net core 無限 loop

如果你是用 asp.net core 並且有啟用 identity 的話可以參考這篇 , 也是很雷
老外說在 client mvc 網站加上以下這段 , 還真的就不會無限 loop 超無言

1
2
3
4
5
builder.Services.AddIdentityCore<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<IdentityUser>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
關閉