identityserver4原始碼解析_3_認證介面
目錄
- 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='xxx' />
<input type='hidden' name='state' value='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 = 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;
}