通过AppAuth-iOS体验Google OIDC服务非常容易,只需要配置下面三个配置项。
static NSString *const kIssuer = @"https://accounts.google.com";
static NSString *const kClientID = @"24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef.apps.googleusercontent.com";
static NSString *const kRedirectURI = @"com.googleusercontent.apps.24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef:/oauth2redirect/";
通过https://accounts.google.com/.well-known/openid-configuration可以获取到Google Auth所有的相关信息,非常丰富的信息。这个配置文件的详细介绍可以参考:OpenID Connect Discovery 1.0 incorporating errata set 1。阿里云和Azure的OIDC Discovery链接如下所示。
- 阿里云:https://oauth.aliyun.com/.well-known/openid-configuration
- Azure:https://login.microsoftonline.com/e86128fb-fc4c-4044-8c6c-98002346bc88/v2.0/.well-known/openid-configuration,格式是
https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration
,tenantId可以去Azure portal里面的AD里面查看到。
从配置信息可以看出Google把auth和login放在accounts.google.com
域下面,token单独放在www.googleapis.com
域下面。
接下来看看如何发起授权。构造OIDAuthorizationRequest请求之后,使用In-App browser(iOS使用SFSafariViewController)打开相应的URL。
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:clientID
clientSecret:clientSecret
scopes:@[ OIDScopeOpenID, OIDScopeProfile ]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
AppDelegate *appDelegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[self logMessage:@"Initiating authorization request %@", request];
appDelegate.currentAuthorizationFlow =
[OIDAuthorizationService presentAuthorizationRequest:request
presentingViewController:self
callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable error) {
if (authorizationResponse) {
OIDAuthState *authState =
[[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse];
[self setAuthState:authState];
[self logMessage:@"Authorization response with code: %@",
authorizationResponse.authorizationCode];
// could just call [self tokenExchange:nil] directly, but will let the user initiate it.
} else {
[self logMessage:@"Authorization error: %@", [error localizedDescription]];
}
}];
- (SFSafariViewController *)safariViewControllerWithURL:(NSURL *)URL {
SFSafariViewController *safariViewController =
[[SFSafariViewController alloc] initWithURL:URL entersReaderIfAvailable:NO];
return safariViewController;
}
本来我还想将SFSafariViewController改成UIWebView,以观察所有URL的流转关系。结果没法使用UIWebView,Google发现是UIWebView会直接报错。
Google Auth返回的id_token已经包含了用户信息,但是还是提供一个单独的userinfo_endpoint去换取用户信息。
手动authorize流程如下所示。
11:09:02: Fetching configuration for issuer: https://accounts.google.com
11:09:02: Got configuration: OIDServiceConfiguration authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth, tokenEndpoint: https://www.googleapis.com/oauth2/v4/token, registrationEndpoint: (null), discoveryDocument: [<OIDServiceDiscovery: 0x608000012970>]
11:09:02: Initiating authorization request <OIDAuthorizationRequest: 0x6000000ac180, request:
//第一步,获取auth code,参数都在URL里面。
https://accounts.google.com/o/oauth2/v2/auth?
response_type=code
&code_challenge_method=S256
&scope=openid%20profile
&code_challenge=zkJhG3SZa9vHx8P8TvikiEozUDNQwlJXnlskEe0wJGA
&redirect_uri=com.googleusercontent.apps.24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef:/oauth2redirect/
&client_id=24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef.apps.googleusercontent.com
&state=KX_4c-UQCxrshPrtHD75CLPoj80gipzxTr1r2hGNhus>
//拿到auth code
11:09:51: Authorization response with code: 4/SS-PVx_JADts0XLrN4VDQiK5QUOTd0qvKtYO_UZJO2E
//第二步,获取access token和refresh token,参数都通过post发出去。
//Google Auth比较有特色的一点是没有client_secret
11:09:52: Performing authorization code exchange with request [<OIDTokenRequest: 0x6080000ac360, request: <URL: https://www.googleapis.com/oauth2/v4/token,
HTTPBody: code=4/SS-PVx_JADts0XLrN4VDQiK5QUOTd0qvKtYO_UZJO2E
&code_verifier=9zUuBzJkfD4y8Ei-424Cx-lWIwObhnSbC5_dOGZXSCk
&redirect_uri=com.googleusercontent.apps.24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef:/oauth2redirect/
&client_id=24408797720-p6d3uj34k564kl4s85o2d1rdgvlp7nef.apps.googleusercontent.com
&grant_type=authorization_code>>]
11:09:53: Received token response with accessToken: ya29.GltBBBYWx0rhh87WWK1oV0peHCI9_EwwoWWf4lUiwBc_--bTXv_Ag6OWdBU6SAHhYx5O6RdOsX1HMCPcELfeIlw-5rESYDuGmyRTqAuchGbHl1Ws-hC10O80YAmi
上面的参数中,code_challenge_method
、code_challenge
、code_verifier
这三个看起来很神秘。它们是为了避免auth code被拦截而做了保护措施。第一步通过code_challenge_method
给code_verifier
做一次计算得到code_challenge
。Authorization Endpoint会保存code_challenge
相关的信息。第二步直接把code_verifier
传递给token endpoint,服务器端做一次对比。如果对不上,就报错。详细信息请参看:Proof Key for Code Exchange by OAuth Public Clients。
继续往下分析一下state这个参数的作用吧。state参数是为了防止CSRF攻击,详细信息请参看:OAuth2:忽略 state 参数引发的 csrf 漏洞 #68。In-App browser回跳到App时,要检查一下URL里面的state是否跟之前的state一致。
OpenID Connect标准集里面包含一个Dynamic Client Registration
的标准,用于动态注册客户端,不过我目前还没有看到哪个OIDC服务器的Discovery里面有registration_endpoint
,包括Google OIDC服务器。
AppAuth客户端是支持这个特性的,很多地方都会判断是否配置了client_id。如果没有配置的话,那么通过Dynamic Client Registration
注册一个客户端。
if (!kClientID) {
[self doClientRegistration:configuration
callback:^(OIDServiceConfiguration *configuration,
OIDRegistrationResponse *registrationResponse) {
[self doAuthWithAutoCodeExchange:configuration
clientID:registrationResponse.clientID
clientSecret:registrationResponse.clientSecret];
}];
} else {
[self doAuthWithAutoCodeExchange:configuration clientID:kClientID clientSecret:nil];
}