Build Status Platforms Documentation

OAuthenticator

轻量级 Swift OAuth 2.0 请求认证库

市面上有很多 OAuth 解决方案。这个库体积小巧,使用 Swift 并发,并提供了对流程的全面控制。

特性

此库目前不具备 JWT 或 JWK 生成功能,而这两者都是 DPoP 所必需的。您必须使用外部 JWT 库来完成此操作,并通过 DPoPSigner.JWTGenerator 函数连接到系统。我已成功使用 jose-swift

还内置了对服务的支持,以简化集成

如果您想为其他服务贡献类似的功能,请开启一个 PR!

集成

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/ChimeHQ/OAuthenticator", from: "0.3.0")
]

用法

主要类型是 Authenticator。它可以像 URLSession 一样执行 URLRequest,但会处理所有身份验证要求并附加所需的 Authorization 标头。其行为由 Authenticator.ConfigurationURLResponseProvider 控制。默认情况下,URLResponseProvider 将是一个私有的 URLSession,但您可以根据需要自定义它。

设置 Configuration 可能会比较麻烦,具体取决于您与之交互的 OAuth 服务。

// backing storage for your authentication data. Without this, tokens will be tied to the lifetime of the `Authenticator`.
let storage = LoginStorage {
    // get login here
} storeLogin: { login in
    // store `login` for later retrieval
}

// application credentials for your OAuth service
let appCreds = AppCredentials(
    clientId: "client_id",
    clientPassword: "client_secret",
    scopes: [],
    callbackURL: URL(string: "my://callback")!
)

// the user authentication function
let userAuthenticator = ASWebAuthenticationSession.userAuthenticator

// functions that define how tokens are issued and refreshed
// This is the most complex bit, as all the pieces depend on exactly how the OAuth-based service works.
// parConfiguration, and dpopJWTGenerator are optional
let tokenHandling = TokenHandling(
    parConfiguration: PARConfiguration(url: parEndpointURL, parameters: extraQueryParams),
    authorizationURLProvider: { params in URL(string: "based on app credentials") }
    loginProvider: { params in ... }
    refreshProvider: { existingLogin, appCreds, urlLoader in ... },
    responseStatusProvider: TokenHandling.refreshOrAuthorizeWhenUnauthorized,
    dpopJWTGenerator: { params in "signed JWT" }
)

let config = Authenticator.Configuration(
    appCredentials: appCreds,
    loginStorage: storage,
    tokenHandling: tokenHandling,
    userAuthenticator: userAuthenticator
)

let authenticator = Authenticator(config: config)

let myRequest = URLRequest(...)

let (data, response) = try await authenticator.response(for: myRequest)

如果您想在不首先发出请求的情况下接收身份验证过程的结果,您可以在 Authenticator.Configuration 初始化器中指定一个可选的 Authenticator.AuthenticationStatusHandler 回调函数。

这允许您支持需要先捕获 Login 对象,然后再执行您的第一个已认证的 URLRequest 并单独管理它的特殊情况。

let authenticationStatusHandler: Authenticator.AuthenticationStatusHandler = { result in
    switch result {
    case .success (let login): 
        authenticatedLogin = login
    case .failure(let error):
        print("Authentication failed: \(error)")
    }
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(
    appCredentials: appCreds,
    tokenHandling: tokenHandling,
    mode: .manualOnly,
    userAuthenticator: userAuthenticator,
    authenticationStatusHandler: authenticationStatusHandler
)

let auth = Authenticator(config: config, urlLoader: mockLoader)
try await auth.authenticate()
if let authenticatedLogin = authenticatedLogin {
    // Process special case
    ...
}

GitHub

OAuthenticator 还附带了针对 GitHub 的预打包配置,这使得设置更加直接。

// pre-configured for GitHub
let appCreds = AppCredentials(clientId: "client_id",
                              clientPassword: "client_secret",
                              scopes: [],
                              callbackURL: URL(string: "my://callback")!)

let config = Authenticator.Configuration(appCredentials: appCreds,
                                         tokenHandling: GitHub.tokenHandling())

let authenticator = Authenticator(config: config)

let myRequest = URLRequest(...)

let (data, response) = try await authenticator.response(for: myRequest)

Mastodon

OAuthenticator 还附带了针对 Mastodon 的预打包配置,这使得设置更加直接。有关更多信息,请查看 https://docs.joinmastodon.org/client/token/

// pre-configured for Mastodon
let userTokenParameters = Mastodon.UserTokenParameters(
    host: "mastodon.social",
    clientName: "MyMastodonApp",
    redirectURI: "myMastodonApp://mastodon/oauth",
    scopes: ["read", "write", "follow"]
)

// The first thing we will need to do is to register an application, in order to be able to generate access tokens later.
// These values will be used to generate access tokens, so they should be cached for later use
let registrationData = try await Mastodon.register(with: userTokenParameters) { request in
    try await URLSession.shared.data(for: request)
}

// Now that we have an application, let’s obtain an access token that will authenticate our requests as that client application.
guard let redirectURI = registrationData.redirectURI, let callbackURL = URL(string: redirectURI) else {
    throw AuthenticatorError.missingRedirectURI
}

let appCreds = AppCredentials(
    clientId: registrationData.clientID,
    clientPassword: registrationData.clientSecret,
    scopes: userTokenParameters.scopes,
    callbackURL: callbackURL
)

let config = Authenticator.Configuration(
    appCredentials: appCreds,
    tokenHandling: Mastodon.tokenHandling(with: userTokenParameters)
)

let authenticator = Authenticator(config: config)

var urlBuilder = URLComponents()
urlBuilder.scheme = Mastodon.scheme
urlBuilder.host = userTokenParameters.host

guard let url = urlBuilder.url else {
    throw AuthenticatorError.missingScheme
}

let request = URLRequest(url: url)

let (data, response) = try await authenticator.response(for: request)

Google API

OAuthenticator 还附带了针对 Google API 的预打包配置(访问 Google Drive、Google People、Google Calendar 等),具体取决于应用程序请求的作用域。

有关这些的更多信息,请访问 Google Workspace。Google OAuth 流程在 Google Identity 中进行了描述

以下是集成示例

// Configuration for Google API

// Define how to store and retrieve the Google Access and Refresh Token
let storage = LoginStorage {
    // Fetch token and return them as a Login object
    return LoginFromSecureStorage(...) 
} storeLogin: { login in
    // Store access and refresh token in Secure storage
    MySecureStorage(login: login)
}

let appCreds = AppCredentials(clientId: googleClientApp.client_id,
                              clientPassword: googleClientApp.client_secret,
                              scopes: googleClientApp.scopes,
                              callbackURL: googleClient.callbackURL)

let config = Authenticator.Configuration(appCredentials: Self.oceanCredentials,
                                         loginStorage: storage,
                                         tokenHandling: tokenHandling,
                                         mode: .automatic)

let authenticator = Authenticator(config: config)

// If you just want the user to authenticate his account and get the tokens, do 1:
// If you want to access a secure Google endpoint with the proper access token, do 2:

// 1: Only Authenticate
try await authenticator.authenticate()

// 2: Access secure Google endpoint (ie: Google Drive: upload a file) with access token
var urlBuilder = URLComponents()
urlBuilder.scheme = GoogleAPI.scheme          // https:
urlBuilder.host = GoogleAPI.host              // www.googleapis.com
urlBuilder.path = GoogleAPI.path              // /upload/drive/v3/files
urlBuilder.queryItems = [
    URLQueryItem(name: GoogleDrive.uploadType, value: "media"),
]

guard let url = urlBuilder.url else {
    throw AuthenticatorError.missingScheme
}

let request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = ...          // File data to upload

let (data, response) = try await authenticator.response(for: request)

Bluesky API

Bluesky 有一个 复杂 的 OAuth 实现。

警告

bsky.social 的 DPoP 随机数 (nonce) 频繁更改(可能每 10-30 秒一次?)。我观察到,如果随机数在用户请求 2FA 代码和代码被输入之间发生更改,服务器将拒绝登录尝试。再次尝试将需要用户交互。

为用户解析 PDS 服务器非常复杂,超出了此库的范围。但是,ATResolve 可能会有所帮助!

let responseProvider = URLSession.defaultProvider
let account = "myhandle.com"
let server = "https://bsky.social"
let clientMetadataEndpoint = "https://example.com/public/facing/client-metadata.json"

// You should know the client configuration, and could general the needed AppCredentials struct manually instead.
// The required fields are "clientId", "callbackURL", and "scopes"
let clientConfig = try await ClientMetadata.load(for: clientMetadataEndpoint, provider: provider)
let serverConfig = try await ServerMetadata.load(for: server, provider: provider)

let jwtGenerator: DPoPSigner.JWTGenerator = { params in
    // generate a P-256 signed token that uses `params` to match the specifications from
    // https://docs.bsky.app/docs/advanced-guides/oauth-client#dpop
}

let tokenHandling = Bluesky.tokenHandling(
    account: account,
    server: serverConfig,
    client: clientConfig,
    jwtGenerator: jwtGenerator
)

let config = Authenticator.Configuration(
    appCredentials: clientConfig.credentials,
    loginStorage: loginStore,
    tokenHandling: tokenHandling
)

let authenticator = Authenticator(config: config)

// you can now use this authenticator to make requests against the user's PDS. Remember, the PDS will not be the same as the authentication server.

贡献和协作

我很乐意听到您的声音!通过 issue 或 pull request 联系我。

我更喜欢协作,如果您有类似的项目,我很乐意找到合作的方式。

我更喜欢使用制表符进行缩进以提高可访问性。但是,我更希望您使用您想要的系统并创建一个 PR,而不是因为空格而犹豫不决。

参与此项目即表示您同意遵守贡献者行为准则