目錄
- IdentityServer4原始碼決議_1_專案結構
- IdentityServer4原始碼決議_2_元資料介面
- IdentityServer4原始碼決議_3_認證介面
- IdentityServer4原始碼決議_4_令牌發放介面
- IdentityServer4原始碼決議_5_查詢用戶資訊介面
- [IdentityServer4原始碼決議_6_結束會話介面]
- [IdentityServer4原始碼決議_7_查詢令牌資訊介面]
- [IdentityServer4原始碼決議_8_撤銷令牌介面]
協議
五種認證方式
-
Authorization Code 授權碼模式:認證服務回傳授權碼,后端用clientid和密鑰向認證服務證明身份,使用授權碼換取id token 和/或 access token,本模式的好處是由后端請求token,不會將敏感資訊暴露在瀏覽器,本模式允許使用refreshToken去維持長時間的登錄狀態,使用此模式的客戶端必須有后端參與,能夠保障客戶端密鑰的安全性,此模式從authorization介面獲取授權碼,從token介面獲取令牌,
-
Implict 簡化模式:校驗跳轉URI驗證客戶端身份之后,直接發放token,通常用于純客戶端應用,如單頁應用javascript客戶端,因為沒有后端參與,密鑰存放在前端是不安全的,由于安全校驗較寬松,本模式不允許使用refreshToken來長時間維持登錄狀態,本模式的所有token從authorization介面獲取,
-
Hybrid 混合流程:混合流程顧名思義組合使用了授權碼模式+簡化模式,前端請求授權服務器回傳授權碼+id_token,這樣前端立刻可以使用用戶的基本資訊;后續請求后端使用授權碼+客戶端密鑰獲取access_token,本模式能夠使用refreshToken來長時間維持登錄狀態,使用本模式必須有后端參與保證客戶端密鑰的安全性,混合模式極少使用,除非你的確需要使用它的某些特性(如一次請求獲取授權碼和用戶資料),一般最常見的還是授權碼模式,
-
Resource Owner Password Credential 用戶名密碼模式:一般用于無用戶互動場景,或者第三方對接(如對接微信登錄,實際登錄界面就變成了微信的界面,如果不希望讓客戶掃了微信之后再跑你們系統登錄一遍,就可以在后端用此模式靜默登錄接上自家的sso即可)
-
Client Credential 客戶端密鑰模式:僅需要約定密鑰,僅用于完全信任的內部系統
認證方式特點對比
| 特點 | 授權碼模式 | 簡化模式 | 混合模式 |
|---|---|---|---|
| 所有token從Authorization介面回傳 | No | Yes | Yes |
| 所有token從Token介面回傳 | Yes | No | No |
| 所有tokens不暴露在瀏覽器 | Yes | No | No |
| 能夠驗證客戶端密鑰 | Yes | No | Yes |
| 能夠使用重繪令牌 | Yes | No | Yes |
| 僅需一次請求 | No | Yes | No |
| 大部分請求由后端進行 | Yes | No | 可變 |
支持回傳型別對比
| 回傳型別 | 認證模式 | 說明 |
|---|---|---|
| code | Authorization Code Flow | 僅回傳授權碼 |
| id_token | Implicit Flow | 回傳身份令牌 |
| id_token token | Implicit Flow | 回傳身份令牌、通行令牌 |
| code id_token | Hybrid Flow | 回傳授權碼、身份令牌 |
| code token | Hybrid Flow | 回傳授權碼、通行令牌 |
| code id_token token | Hybrid Flow | 回傳授權碼、身份令牌、通行令牌 |
授權碼模式決議
相對來說,授權碼模式還是用的最多的,我們詳細解讀一下本模式的協議內容,
授權時序圖
sequenceDiagram 用戶->>客戶端: 請求受保護資源 客戶端->>認證服務: 準備入參,發起認證請求 認證服務->>認證服務: 認證用戶 認證服務->>用戶: 是否同意授權 認證服務->>客戶端: 發放授權碼(前端進行) 客戶端->>認證服務: 使用授權碼請求token(后端進行) 認證服務->>認證服務: 校驗客戶端密鑰,校驗授權碼 認證服務->>客戶端: 發放身份令牌、通行令牌(后端進行) 客戶端->>客戶端: 校驗身份令牌,獲取用戶標識認證請求
認證介面必須同時支持GET和POST兩種請求方式,如果使用GET方法,客戶端必須使用URI Query傳遞引數,如果使用POST方法,客戶端必須使用Form傳遞引數,
引數定義
- scope:授權范圍,必填,必須包含openid,
- response_type:回傳型別,必填,定義了認證服務回傳哪些引數,對于授權碼模式,本引數只能是code,
- client_id:客戶端id,必填,
- redirect_uri:跳轉地址,必填,授權碼生成之后,認證服務會帶著授權碼和其他引數回跳到此地址,此地址要求使用https,如果使用http,則客戶端型別必須是confidential,
- state:狀態欄位,推薦填寫,一般用于客戶端與認證服務比對此欄位,來防跨站偽造攻擊,同時state也可以存放狀態資訊,如發起認證時的頁面地址,用于認證完成后回到原始頁面,
- 其他:略,上面五個是和OAuth2.0一樣的引數,oidc還定義了一些擴展引數,用的很少,不是很懂,感興趣的自己去看協議,
請求報文示例
HTTP/1.1 302 Found
Location: https://server.example.com/authorize?
response_type=code
&scope=openid%20profile%20email
&client_id=s6BhdRkqt3
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
認證請求校驗
- 必填校驗
- response_type必須為code
- scope必填,必須包含openid
認證終端用戶
- 下面兩種情況認證服務必須認證用戶
- 用戶尚未認證
- 認證請求包含引數prompt=login,即使用戶已經認證過也需要重新認證
- 認證請求包含引數prompt=none,然后用戶尚未被認證,則需要回傳錯誤資訊
認證服務必須想辦法防止程序中的跨站偽造攻擊和點擊劫持攻擊,
獲取終端用戶授權/同意
終端用戶通過認證之后,認證服務必須與終端用戶互動,詢問用戶是否同意對客戶端的授權,
認證回應
成功回應
使用 application/x-www-form-urlencoded格式回傳結果
例如:
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
code=SplxlOBeZQQYbYS6WxSbIA
&state=af0ifjsldkj
失敗回應
錯誤代碼包括這些
oauth2.0定義的回應代碼
- invalid_request:非法請求,未提供必填引數,引數非法等情況
- unauthorized_client:客戶端未授權
- access_denied:用戶無權限
- unsupported_response_type
- invalid_scope:非法的scope引數
- server_error
- temporarily_unavailable
另外oidc還擴展了一些回應代碼,不常見,略
例如:
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
error=invalid_request
&error_description=
Unsupported%20response_type%20value
&state=af0ifjsldkj
客戶端校驗授權碼
協議規定客戶端必須校驗授權碼的正確性
原始碼決議
從AuthorizeEndpoint的ProcessAsync方法作為入口開始認證介面的原始碼決議,
- 判斷請求方式是GET還是POST,獲取入參,如果是其他請求方式415狀態碼
- 從session中獲取user
- 入參和user作為入參,呼叫父類ProcessAuthorizeRequestAsync方法
public override async Task<IEndpointResult> ProcessAsync(HttpContext context)
{
Logger.LogDebug("Start authorize request");
NameValueCollection values;
if (HttpMethods.IsGet(context.Request.Method))
{
values = context.Request.Query.AsNameValueCollection();
}
else if (HttpMethods.IsPost(context.Request.Method))
{
if (!context.Request.HasFormContentType)
{
return new StatusCodeResult(HttpStatusCode.UnsupportedMediaType);
}
values = context.Request.Form.AsNameValueCollection();
}
else
{
return new StatusCodeResult(HttpStatusCode.MethodNotAllowed);
}
var user = await UserSession.GetUserAsync();
var result = await ProcessAuthorizeRequestAsync(values, user, null);
Logger.LogTrace("End authorize request. result type: {0}", result?.GetType().ToString() ?? "-none-");
return result;
}
認證站點如果cookie中存在當前會話資訊,則直接回傳用戶資訊,否則呼叫cookie架構的認證方法,會跳轉到登錄頁面,
public virtual async Task<ClaimsPrincipal> GetUserAsync()
{
await AuthenticateAsync();
return Principal;
}
protected virtual async Task AuthenticateAsync()
{
if (Principal == null || Properties == null)
{
var scheme = await GetCookieSchemeAsync();
var handler = await Handlers.GetHandlerAsync(HttpContext, scheme);
if (handler == null)
{
throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}");
}
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
{
Principal = result.Principal;
Properties = result.Properties;
}
}
}
認證請求處理流程大致分為三步
- AuthorizeRequestValidator校驗所有引數
- 認證介面consent入參為null,不需要處理用戶互動判斷
- 生成回傳報文
internal async Task<IEndpointResult> ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, ConsentResponse consent)
{
if (user != null)
{
Logger.LogDebug("User in authorize request: {subjectId}", user.GetSubjectId());
}
else
{
Logger.LogDebug("No user present in authorize request");
}
// validate request
var result = await _validator.ValidateAsync(parameters, user);
if (result.IsError)
{
return await CreateErrorResultAsync(
"Request validation failed",
result.ValidatedRequest,
result.Error,
result.ErrorDescription);
}
var request = result.ValidatedRequest;
LogRequest(request);
// determine user interaction
var interactionResult = await _interactionGenerator.ProcessInteractionAsync(request, consent);
if (interactionResult.IsError)
{
return await CreateErrorResultAsync("Interaction generator error", request, interactionResult.Error, interactionResult.ErrorDescription, false);
}
if (interactionResult.IsLogin)
{
return new LoginPageResult(request);
}
if (interactionResult.IsConsent)
{
return new ConsentPageResult(request);
}
if (interactionResult.IsRedirect)
{
return new CustomRedirectResult(request, interactionResult.RedirectUrl);
}
var response = await _authorizeResponseGenerator.CreateResponseAsync(request);
await RaiseResponseEventAsync(response);
LogResponse(response);
return new AuthorizeResult(response);
}
生成回傳資訊
此處只有AuthorizationCode、Implicit、Hybrid三種授權型別的判斷,用戶名密碼、客戶端密鑰模式不能使用authorize介面,
public virtual async Task<AuthorizeResponse> CreateResponseAsync(ValidatedAuthorizeRequest request)
{
if (request.GrantType == GrantType.AuthorizationCode)
{
return await CreateCodeFlowResponseAsync(request);
}
if (request.GrantType == GrantType.Implicit)
{
return await CreateImplicitFlowResponseAsync(request);
}
if (request.GrantType == GrantType.Hybrid)
{
return await CreateHybridFlowResponseAsync(request);
}
Logger.LogError("Unsupported grant type: " + request.GrantType);
throw new InvalidOperationException("invalid grant type: " + request.GrantType);
}
- 如果state欄位不為空,使用加密演算法得到state的hash值
- 構建AuthorizationCode物件,存放在store中,store是idsv4用于持久化的物件,默認實作存盤在記憶體中,可以對可插拔服務進行注入替換,實作資料保存在在mysql、redis等流行存盤中
- 將授權碼物件的id回傳
protected virtual async Task<AuthorizeResponse> CreateCodeFlowResponseAsync(ValidatedAuthorizeRequest request)
{
Logger.LogDebug("Creating Authorization Code Flow response.");
var code = await CreateCodeAsync(request);
var id = await AuthorizationCodeStore.StoreAuthorizationCodeAsync(code);
var response = new AuthorizeResponse
{
Request = request,
Code = id,
SessionState = request.GenerateSessionStateValue()
};
return response;
}
protected virtual async Task<AuthorizationCode> CreateCodeAsync(ValidatedAuthorizeRequest request)
{
string stateHash = null;
if (request.State.IsPresent())
{
var credential = await KeyMaterialService.GetSigningCredentialsAsync();
if (credential == null)
{
throw new InvalidOperationException("No signing credential is configured.");
}
var algorithm = credential.Algorithm;
stateHash = CryptoHelper.CreateHashClaimValue(request.State, algorithm);
}
var code = new AuthorizationCode
{
CreationTime = Clock.UtcNow.UtcDateTime,
ClientId = request.Client.ClientId,
Lifetime = request.Client.AuthorizationCodeLifetime,
Subject = request.Subject,
SessionId = request.SessionId,
CodeChallenge = request.CodeChallenge.Sha256(),
CodeChallengeMethod = request.CodeChallengeMethod,
IsOpenId = request.IsOpenIdRequest,
RequestedScopes = request.ValidatedScopes.GrantedResources.ToScopeNames(),
RedirectUri = request.RedirectUri,
Nonce = request.Nonce,
StateHash = stateHash,
WasConsentShown = request.WasConsentShown
};
return code;
}
回傳結果
- 如果ResponseMode等于Query或者Fragment,將授權碼code及其他資訊拼裝到Uri,回傳302重定向請求
例子:
302 https://mysite.com?code=xxxxx&state=xxx
- 如果是FormPost方式,會生成一段腳本回傳到客戶端,視窗加載會觸發form表單提交,將code、state等資訊包裹在隱藏欄位里提交到配置的rediret_uri,
<html>
<head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<base target='_self'/>
</head>
<body>
<form method='post' action='https://mysite.com'>
<input type='hidden' name='code' value='https://www.cnblogs.com/holdengong/p/xxx' />
<input type='hidden' name='state' value='https://www.cnblogs.com/holdengong/p/xxx' />
<noscript>
<button>Click to continue</button>
</noscript>
</form>
<script>window.addEventListener('load', function(){document.forms[0].submit();});</script>
</body>
</html>
private async Task RenderAuthorizeResponseAsync(HttpContext context)
{
if (Response.Request.ResponseMode == OidcConstants.ResponseModes.Query ||
Response.Request.ResponseMode == OidcConstants.ResponseModes.Fragment)
{
context.Response.SetNoCache();
context.Response.Redirect(BuildRedirectUri());
}
else if (Response.Request.ResponseMode == OidcConstants.ResponseModes.FormPost)
{
context.Response.SetNoCache();
AddSecurityHeaders(context);
await context.Response.WriteHtmlAsync(GetFormPostHtml());
}
else
{
//_logger.LogError("Unsupported response mode.");
throw new InvalidOperationException("Unsupported response mode");
}
}
客戶端在回呼地址接收code,即可向token介面換取token,
其他
簡單看一下簡化流程和混合流程是怎么創建回傳報文的,
簡化流程生成回傳報文
- 如果回傳型別包含token,生成通行令牌
- 如果回傳型別包含id_token,生成身份令牌
可以看到,簡化流程的所有token都是由authorization介面回傳的,一次請求回傳所有token,
protected virtual async Task<AuthorizeResponse> CreateImplicitFlowResponseAsync(ValidatedAuthorizeRequest request, string authorizationCode = null)
{
Logger.LogDebug("Creating Implicit Flow response.");
string accessTokenValue = https://www.cnblogs.com/holdengong/p/null;
int accessTokenLifetime = 0;
var responseTypes = request.ResponseType.FromSpaceSeparatedString();
if (responseTypes.Contains(OidcConstants.ResponseTypes.Token))
{
var tokenRequest = new TokenCreationRequest
{
Subject = request.Subject,
Resources = request.ValidatedScopes.GrantedResources,
ValidatedRequest = request
};
var accessToken = await TokenService.CreateAccessTokenAsync(tokenRequest);
accessTokenLifetime = accessToken.Lifetime;
accessTokenValue = await TokenService.CreateSecurityTokenAsync(accessToken);
}
string jwt = null;
if (responseTypes.Contains(OidcConstants.ResponseTypes.IdToken))
{
string stateHash = null;
if (request.State.IsPresent())
{
var credential = await KeyMaterialService.GetSigningCredentialsAsync();
if (credential == null)
{
throw new InvalidOperationException("No signing credential is configured.");
}
var algorithm = credential.Algorithm;
stateHash = CryptoHelper.CreateHashClaimValue(request.State, algorithm);
}
var tokenRequest = new TokenCreationRequest
{
ValidatedRequest = request,
Subject = request.Subject,
Resources = request.ValidatedScopes.GrantedResources,
Nonce = request.Raw.Get(OidcConstants.AuthorizeRequest.Nonce),
IncludeAllIdentityClaims = !request.AccessTokenRequested,
AccessTokenToHash = accessTokenValue,
AuthorizationCodeToHash = authorizationCode,
StateHash = stateHash
};
var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest);
jwt = await TokenService.CreateSecurityTokenAsync(idToken);
}
var response = new AuthorizeResponse
{
Request = request,
AccessToken = accessTokenValue,
AccessTokenLifetime = accessTokenLifetime,
IdentityToken = jwt,
SessionState = request.GenerateSessionStateValue()
};
return response;
}
混合流程生成回傳報文
這段代碼充分體現了它為啥叫混合流程,把生成授權碼的方法調一遍,再把簡化流程的方法調一遍,code和token可以一起回傳,
protected virtual async Task<AuthorizeResponse> CreateHybridFlowResponseAsync(ValidatedAuthorizeRequest request)
{
Logger.LogDebug("Creating Hybrid Flow response.");
var code = await CreateCodeAsync(request);
var id = await AuthorizationCodeStore.StoreAuthorizationCodeAsync(code);
var response = await CreateImplicitFlowResponseAsync(request, id);
response.Code = id;
return response;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/50962.html
標籤:.NET Core
