适用于 iOS、macOS 和 tvOS 的 AppAuth 是一个用于与 OAuth 2.0 和 OpenID Connect 提供程序通信的客户端 SDK。它力求直接映射这些规范中的请求和响应,同时遵循实现语言的传统风格。除了映射原始协议流程之外,还有方便的方法可以帮助执行常见任务,如使用新鲜令牌执行操作。
它遵循在 RFC 8252 - 本地应用的 OAuth 2.0 中设定的最佳实践,包括在 iOS 上使用 SFAuthenticationSession
和 SFSafariViewController
进行身份验证请求。UIWebView
和 WKWebView
由于在 RFC 8252 8.12 节中解释的安全性和可用性问题而被显式不支持。
它还支持 OAuth 的 PKCE 扩展,该扩展是为了在使用自定义 URI 计划重新定向时保护公开客户端中的授权代码而创建的。此库对其他扩展(标准或其他)都很友好,能够在所有协议请求和响应中处理额外的参数。
对于 tvOS,AppAuth 实现了 OAuth 2.0 设备授权许可,允许多通过二级设备进行 tvOS 登录。
规范
iOS
支持版本
AppAuth支持iOS 7及以上版本。
iOS 9及以上版本使用应用内浏览器标签模式(通过SFSafariViewController
),在早期版本上回退到系统浏览器(移动Safari)。
授权服务器要求
库支持使用自定义URI方案(所有iOS支持的版本)和通用链接(iOS 9+)。
通常,AppAuth可以与任何支持本地应用的授权服务器一起工作,如RFC 8252中所记录,无论是通过自定义URI方案重定向还是通用链接。假设所有客户端都是基于Web的或要求客户端维护客户端密钥机密的授权服务器可能无法正常工作。
macOS
支持版本
AppAuth支持macOS (OS X) 10.9及以上版本。
授权服务器要求
AppAuth for macOS支持自定义方案;通过一个小型嵌入服务器进行循环回跳的HTTP重定向。
一般来说,AppAuth可以与支持本地应用的任何授权服务器一起使用,如RFC 8252中所记录;通过自定义URI方案或循环回跳的HTTP重定向。假设所有客户端都是基于Web的授权服务器,或要求客户端维护客户端机密的保密性,可能不起作用。
tvOS
支持版本
AppAuth支持tvOS 9.0及以上版本。请注意,虽然在tvOS上可能运行标准的AppAuth库,但以下文档描述了实现OAuth 2.0设备授权授予(AppAuthTV)。
授权服务器要求
AppAuthTV旨在为支持RFC 8628中记录的设备授权流程的服务器设计。
尝试
想要尝试AppAuth?只需运行
pod try AppAuth
按照Examples/README.md中的说明配置您自己的OAuth客户端(您需要用客户端信息更新三个配置点来尝试演示)。
设置
AppAuth支持四种依赖关系管理选项。
CocoaPods
使用CocoaPods,将以下行添加到您的Podfile
pod 'AppAuth'
然后,运行pod install
。
tvOS: 使用TV
子规范
pod 'AppAuth/TV'
Swift包管理器
使用Swift包管理器,将以下dependency
添加到您的Package.swift
dependencies: [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.3.0"))
]
tvOS: 使用AppAuthTV
目标。
Carthage
使用 Carthage,向您的 Cartfile
中添加以下行
github "openid/AppAuth-iOS" "master"
然后,运行 carthage bootstrap
。
tvOS: 使用 AppAuthTV
框架。
静态库
您还可以将 AppAuth 作为静态库使用。这需要链接库和您的项目,并包含头文件。以下是一个建议的配置
- 创建一个 Xcode 工作空间。
- 将
AppAuth.xcodeproj
添加到您的 Workspace 中。 - 将 libAppAuth 作为链接库添加到您的目标(在您的目标的 "通用 -> 链接框架和库" 部分)。
- 将
AppAuth-iOS/Source
添加到您的目标的搜索路径中(在 "构建设置 -> "头文件搜索路径" 中)。
注意:没有 AppAuthTV 的静态库。
身份验证流程
AppAuth 支持与授权服务器进行手动交互,您需要进行自己的令牌交换,以及一些便捷方法,这些方法为您执行部分逻辑。此示例使用的是便捷方法,它返回一个 OIDAuthState
对象或一个错误。
OIDAuthState
是一个类,用于跟踪授权和令牌请求及响应,并提供一个便捷方法来调用带有新鲜令牌的 API。这是您需要序列化的唯一对象,以保留会话的授权状态。
配置
您可以通过指定端点来直接配置AppAuth
Objective-C
NSURL *authorizationEndpoint =
[NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDServiceConfiguration *configuration =
[[OIDServiceConfiguration alloc]
initWithAuthorizationEndpoint:authorizationEndpoint
tokenEndpoint:tokenEndpoint];
// perform the auth request...
Swift
let authorizationEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!
let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")!
let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint,
tokenEndpoint: tokenEndpoint)
// perform the auth request...
tvOS
Objective-C
NSURL *deviceAuthorizationEndpoint =
[NSURL URLWithString:@"https://oauth2.googleapis.com/device/code"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDTVServiceConfiguration *configuration =
[[OIDTVServiceConfiguration alloc]
initWithDeviceAuthorizationEndpoint:deviceAuthorizationEndpoint
tokenEndpoint:tokenEndpoint];
// perform the auth request...
或者通过发现
Objective-C
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer
completion:^(OIDServiceConfiguration *_Nullable configuration,
NSError *_Nullable error) {
if (!configuration) {
NSLog(@"Error retrieving discovery document: %@",
[error localizedDescription]);
return;
}
// perform the auth request...
}];
Swift
let issuer = URL(string: "https://accounts.google.com")!
// discovers endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
guard let config = configuration else {
print("Error retrieving discovery document: \(error?.localizedDescription ?? "Unknown error")")
return
}
// perform the auth request...
}
tvOS
Objective-C
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDTVAuthorizationService discoverServiceConfigurationForIssuer:issuer
completion:^(OIDTVServiceConfiguration *_Nullable configuration,
NSError *_Nullable error) {
if (!configuration) {
NSLog(@"Error retrieving discovery document: %@",
[error localizedDescription]);
return;
}
// perform the auth request...
}];
授权 – iOS
首先,您需要在您的 UIApplicationDelegate
实现中有一个属性来保存会话,以便从重定向继续授权流程。在这个例子中,这个代理的实现是一个名为 AppDelegate
的类,如果您的应用的应用代理有不同的名称,请相应地更新以下示例中的类名。
Objective-C
@interface AppDelegate : UIResponder <UIApplicationDelegate>
// property of the app's AppDelegate
@property(nonatomic, strong, nullable) id<OIDExternalUserAgentSession> currentAuthorizationFlow;
@end
Swift
class AppDelegate: UIResponder, UIApplicationDelegate {
// property of the app's AppDelegate
var currentAuthorizationFlow: OIDExternalUserAgentSession?
}
并且您的main类,一个属性来存储-auth状态
Objective-C
// property of the containing class
@property(nonatomic, strong, nullable) OIDAuthState *authState;
Swift
// property of the containing class
private var authState: OIDAuthState?
然后,启动授权请求。通过使用 authStateByPresentingAuthorizationRequest
便利方法,将会自动执行令牌交换,并且所有内容都将使用PKCE进行保护(如果服务器支持)。AppAuth还允许您手动执行这些请求。请参阅随附示例应用中的 authNoCodeExchange
方法以进行演示
Objective-C
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
scopes:@[OIDScopeOpenID,
OIDScopeProfile]
redirectURL:kRedirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
AppDelegate *appDelegate =
(AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
presentingViewController:self
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
[self setAuthState:authState];
} else {
NSLog(@"Authorization error: %@", [error localizedDescription]);
[self setAuthState:nil];
}
}];
Swift
// builds authentication request
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: redirectURI,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// performs authentication request
print("Initiating authorization request with scope: \(request.scope ?? "nil")")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.currentAuthorizationFlow =
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
print("Got authorization tokens. Access token: " +
"\(authState.lastTokenResponse?.accessToken ?? "nil")")
} else {
print("Authorization error: \(error?.localizedDescription ?? "Unknown error")")
self.setAuthState(nil)
}
}
处理重定向
授权响应URL是通过iOS openURL应用代理方法返回到应用程序的,因此您需要将它传递到当前的授权会话(在之前的会话中创建的)
Objective-C
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<NSString *, id> *)options {
// Sends the URL to the current authorization flow (if any) which will
// process it if it relates to an authorization response.
if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
_currentAuthorizationFlow = nil;
return YES;
}
// Your additional URL handling (if any) goes here.
return NO;
}
Swift
func application(_ app: UIApplication,
open url: URL,
options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// Sends the URL to the current authorization flow (if any) which will
// process it if it relates to an authorization response.
if let authorizationFlow = self.currentAuthorizationFlow,
authorizationFlow.resumeExternalUserAgentFlow(with: url) {
self.currentAuthorizationFlow = nil
return true
}
// Your additional URL handling (if any)
return false
}
授权 – MacOS
在macOS上,获取授权响应重定向最流行的方法是在回环接口上启动本地HTTP服务器(仅限于用户机器的入站请求)。授权完成时,用户会被重定向到该本地服务器,并由应用程序处理授权响应。AppAuth会为您管理本地HTTP服务器生命周期。
💡 替代方案:自定义URI方案macOS也支持自定义URI方案,但某些浏览器会显示一个中间页面,这会降低用户体验。有关使用自定义URI方案进行macOS的示例,请参阅
Example-Mac
。
要使用本地HTTP服务器接收授权响应,首先您需要在您的main类中有一个实例变量来保存HTTP重定向处理程序
Objective-C
OIDRedirectHTTPHandler *_redirectHTTPHandler;
然后,由于本地HTTP服务器使用的端口号可能不同,你需要在构建授权请求之前启动它,以便获取要使用的确切重定向URI。
Objective-C
static NSString *const kSuccessURLString =
@"http://openid.github.io/AppAuth-iOS/redirect/";
NSURL *successURL = [NSURL URLWithString:kSuccessURLString];
// Starts a loopback HTTP redirect listener to receive the code. This needs to be started first,
// as the exact redirect URI (including port) must be passed in the authorization request.
_redirectHTTPHandler = [[OIDRedirectHTTPHandler alloc] initWithSuccessURL:successURL];
NSURL *redirectURI = [_redirectHTTPHandler startHTTPListener:nil];
然后,初始化授权请求。通过使用authStateByPresentingAuthorizationRequest
便捷方法,将自动执行令牌交换,并且一切都会通过PKCE进行保护(如果服务器支持)。将返回值分配给OIDRedirectHTTPHandler
的currentAuthorizationFlow
,用户做出选择后授权将自动继续。
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
clientSecret:kClientSecret
scopes:@[ OIDScopeOpenID ]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
__weak __typeof(self) weakSelf = self;
_redirectHTTPHandler.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
// Brings this app to the foreground.
[[NSRunningApplication currentApplication]
activateWithOptions:(NSApplicationActivateAllWindows |
NSApplicationActivateIgnoringOtherApps)];
// Processes the authorization response.
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
} else {
NSLog(@"Authorization error: %@", error.localizedDescription);
}
[weakSelf setAuthState:authState];
}];
作者化 – tvOS
请确保您的主类是OIDAuthStateChangeDelegate
、OIDAuthStateErrorDelegate
的代理,实现相应的方法,并包含以下属性和实例变量
Objective-C
// property of the containing class
@property(nonatomic, strong, nullable) OIDAuthState *authState;
// instance variable of the containing class
OIDTVAuthorizationCancelBlock _cancelBlock;
然后,构建和执行授权请求。
Objective-C
// builds authentication request
__weak __typeof(self) weakSelf = self;
OIDTVAuthorizationRequest *request =
[[OIDTVAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
clientSecret:kClientSecret
scopes:@[ OIDScopeOpenID, OIDScopeProfile ]
additionalParameters:nil];
// performs authentication request
OIDTVAuthorizationInitialization initBlock =
^(OIDTVAuthorizationResponse *_Nullable response, NSError *_Nullable error) {
if (response) {
// process authorization response
NSLog(@"Got authorization response: %@", response);
} else {
// handle initialization error
NSLog(@"Error: %@", error);
}
};
OIDTVAuthorizationCompletion completionBlock =
^(OIDAuthState *_Nullable authState, NSError *_Nullable error) {
weakSelf.signInView.hidden = YES;
if (authState) {
NSLog(@"Token response: %@", authState.lastTokenResponse);
[weakSelf setAuthState:authState];
} else {
NSLog(@"Error: %@", error);
[weakSelf setAuthState:nil];
}
};
_cancelBlock = [OIDTVAuthorizationService authorizeTVRequest:request
initialization:initBlock
completion:completionBlock];
进行API调用
AppAuth提供了原始令牌信息,如果您需要它。然而,我们建议使用OIDAuthState
便捷包装器的人使用提供的方法performActionWithFreshTokens:
来执行他们的API调用,以避免担心令牌的新鲜度
Objective-C
[_authState performActionWithFreshTokens:^(NSString *_Nonnull accessToken,
NSString *_Nonnull idToken,
NSError *_Nullable error) {
if (error) {
NSLog(@"Error fetching fresh tokens: %@", [error localizedDescription]);
return;
}
// perform your API request using the tokens
}];
Swift
let userinfoEndpoint = URL(string:"https://openidconnect.googleapis.com/v1/userinfo")!
self.authState?.performAction() { (accessToken, idToken, error) in
if error != nil {
print("Error fetching fresh tokens: \(error?.localizedDescription ?? "Unknown error")")
return
}
guard let accessToken = accessToken else {
return
}
// Add Bearer token to request
var urlRequest = URLRequest(url: userinfoEndpoint)
urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"]
// Perform request...
}
自定义用户代理(iOS和macOS)
每个OAuth流程都涉及到向用户提供外部用户代理,使他们能够与OAuth授权服务器进行交互。用户代理的典型示例是用户的浏览器,或者在iOS上的类似ASWebAuthenticationSession
的本地浏览器标签实现。
AppAuth自带几个外部用户代理实现,包括适用于大多数情况的iOS和macOS默认设置。默认用户代理通常与系统默认浏览器共享持久cookies,以提高用户不必再次登录的可能性。
您可以更改AppAuth使用的外部用户代理,甚至可以自己编写,而无需分叉库。
所有外部用户代理的实现,不管是包含的还是由您创建的,都需要遵守OIDExternalUserAgent
协议。
将 OIDExternalUserAgent
实例传递给 OIDAuthState.authStateByPresentingAuthorizationRequest:externalUserAgent:callback
和 / 或 OIDAuthorizationService.presentAuthorizationRequest:externalUserAgent:callback:
,而不是使用平台特定的便捷方法(这些方法使用各自的平台的默认用户代理),如 OIDAuthState.authStateByPresentingAuthorizationRequest:presentingViewController:callback:
。
编写自己的用户代理实现的一些常用场景包括需要以 AppAuth 不支持的方式对用户代理进行样式设计,以及实现完全定制的流程,包含自己的业务逻辑。您可以将现有的实现之一作为起点进行复制、重命名和定制以满足您的需求。
自定义浏览器用户代理
AppAuth for iOS 包含了一些额外的用户代理实现,您可以尝试使用,或将其作为您自己实现的参考。其中之一,OIDExternalUserAgentIOSCustomBrowser
允许您在认证时使用不同的浏览器,如 iOS 的 Chrome 或 Firefox。
以下是使用 OIDExternalUserAgentIOSCustomBrowser
用户代理配置 AppAuth 以使用自定义浏览器的步骤
首先,将以下数组添加到您的 Info.plist(在 XCode 中,右键单击 -> 以源代码打开)
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>opera-https</string>
<string>firefox</string>
</array>
这是必须的,以便 AppAuth 可以检测浏览器,并在未安装的情况下打开应用商店(此用户代理的默认行为)。您只需要包含您打算使用的实际浏览器的 URL 方案。
Objective-C
// performs authentication request
AppDelegate *appDelegate =
(AppDelegate *)[UIApplication sharedApplication].delegate;
id<OIDExternalUserAgent> userAgent =
[OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome];
appDelegate.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
externalUserAgent:userAgent
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
[self setAuthState:authState];
} else {
NSLog(@"Authorization error: %@", [error localizedDescription]);
[self setAuthState:nil];
}
}];
Swift
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
let userAgent = OIDExternalUserAgentIOSCustomBrowser.customBrowserChrome()
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, externalUserAgent: userAgent) { authState, error in
if let authState = authState {
self.setAuthState(authState)
self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
} else {
self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
}
}
这就完成了!有了这两个更改(您可以在包含的示例中尝试这两个更改),AppAuth 将使用 Chrome iOS 进行授权请求(如果未安装,将在应用商店中打开 Chrome)。
OIDExternalUserAgentIOSCustomBrowser
用户代理不是针对消费类应用设计的。它设计用于高级企业级用例,其中应用开发者对操作环境有更多控制,并有特殊需求,需要像 Chrome 这样的自定义浏览器。
您也不必只使用包含的外部用户代理!由于 OIDExternalUserAgent
协议是 AppAuth 公共 API 的一部分,您可以实现自己的版本。在上面的示例中,将用您的用户代理实现实例替换 userAgent = [OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome]
。
API 文档
浏览API 文档。
包含的示例
探索 AppAuth 核心功能的示例应用程序适用于 iOS、macOS 和 tvOS;按照 Examples/README.md 中的说明开始使用。