AppAuth for iOS and macOS tests codecov Carthage compatible SwiftPM compatible Pod Version Pod License Pod Platform Catalyst compatible

AppAuth for iOS、macOS 和 tvOS 是一个客户端 SDK,用于与 OAuth 2.0OpenID Connect 提供商通信。它力求直接映射这些规范的请求和响应,同时遵循实现语言的惯用风格。除了映射原始协议流程之外,还提供便捷方法来辅助执行常用任务,例如使用新的令牌执行操作。

它遵循 RFC 8252 - 针对原生应用的 OAuth 2.0 中规定的最佳实践,包括在 iOS 上使用 SFAuthenticationSessionSFSafariViewController 进行授权请求。由于 RFC 8252 第 8.12 节 中解释的安全性和可用性原因,明确支持 UIWebViewWKWebView

它还支持 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 及更高版本。

授权服务器要求

macOS 版 AppAuth 支持自定义方案;通过小型嵌入式服务器进行环回 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 Package Manager

使用 Swift Package Manager,将以下 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 用作静态库。这需要链接库和您的项目,并包含头文件。以下是建议的配置

  1. 创建一个 Xcode Workspace。
  2. AppAuth.xcodeproj 添加到您的 Workspace。
  3. 将 libAppAuth 作为链接库包含到您的目标中(在目标的“General -> Linked Framework and Libraries”部分)。
  4. AppAuth-iOS/Source 添加到您的目标的搜索路径(“Build Settings -> "Header Search Paths"”)。

注意:AppAuthTV 没有静态库。

授权流程

AppAuth 支持与授权服务器的手动交互,您需要执行自己的令牌交换,以及为您执行部分此逻辑的便捷方法。此示例使用便捷方法,该方法返回 OIDAuthState 对象或错误。

OIDAuthState 是一个类,用于跟踪授权和令牌请求与响应,并提供一个便捷方法来使用新的令牌调用 API。这是您需要序列化以保留会话授权状态的唯一对象。

配置

您可以通过直接指定端点来配置 AppAuth

Objective-C

NSURL *authorizationEndpoint =
    [NSURL URLWithString:@"https://#/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://#/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://#"];

[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://#")!

// 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://#"];

[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?
}

以及您的主类,一个用于存储授权状态的属性

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 方案,但某些浏览器会显示插页式广告,这会降低可用性。有关在 macOS 上使用自定义 URI 方案的示例,请参阅 Example-Mac

要使用本地 HTTP 服务器接收授权响应,首先您需要在主类中设置一个实例变量来保留 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 的保护(如果服务器支持)。通过将返回值分配给 OIDRedirectHTTPHandlercurrentAuthorizationFlow,一旦用户做出选择,授权将自动继续

// 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

确保您的主类是 OIDAuthStateChangeDelegateOIDAuthStateErrorDelegate 的委托,实现相应的方法,并包含以下属性和实例变量

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
                                           additionalHeaders: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 默认值。默认用户代理通常与系统默认浏览器共享持久性 Cookie,以提高用户无需再次登录的可能性。

可以更改 AppAuth 使用的用户代理,甚至编写您自己的用户代理 - 所有这些都无需 fork 库。

外部用户代理的所有实现,无论是包含的还是您创建的,都需要符合 OIDExternalUserAgent 协议。

OIDExternalUserAgent 的实例被传递到 OIDAuthState.authStateByPresentingAuthorizationRequest:externalUserAgent:callback 和/或 OIDAuthorizationService.presentAuthorizationRequest:externalUserAgent:callback:,而不是使用平台特定的便捷方法(这些方法为其各自平台使用默认用户代理),例如 OIDAuthState.authStateByPresentingAuthorizationRequest:presentingViewController:callback:

编写您自己的用户代理实现的热门用例包括需要以 AppAuth 不支持的方式设置用户代理的样式,以及使用您自己的业务逻辑实现完全自定义的流程。您可以将现有实现之一作为起点进行复制、重命名和自定义以满足您的需求。

自定义浏览器用户代理

iOS 版 AppAuth 包含一些额外的用户代理实现,您可以尝试使用,或将其用作您自己实现的参考。其中之一,OIDExternalUserAgentIOSCustomBrowser 使您可以使用不同的浏览器进行身份验证,例如 iOS 版 Chrome 或 iOS 版 Firefox。

以下是如何配置 AppAuth 以使用自定义浏览器,使用 OIDExternalUserAgentIOSCustomBrowser 用户代理

首先,将以下数组添加到您的 Info.plist(在 XCode 中,右键单击 -> Open As -> Source Code)

    <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 时在 App Store 中打开 Chrome)。

⚠️注意:OIDExternalUserAgentIOSCustomBrowser 用户代理不适用于消费者应用。它专为高级企业用例而设计,在这些用例中,应用开发者对操作环境具有更大的控制权,并且有需要自定义浏览器(如 Chrome)的特殊要求。

您也不需要止步于包含的外部用户代理!由于 OIDExternalUserAgent 协议是 AppAuth 公共 API 的一部分,因此您可以实现自己的版本。在上面的示例中,userAgent = [OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome] 将被替换为您用户代理实现的实例化。

API 文档

浏览 API 文档

包含的示例

适用于 iOS、macOS 和 tvOS 的示例应用程序探索了 AppAuth 的核心功能;按照 Examples/README.md 中的说明开始使用。