轻量级 Swift OAuth 2.0 请求认证库
市面上有很多 OAuth 解决方案。这个库体积小巧,使用 Swift 并发,并提供了对流程的全面控制。
特性
ASWebAuthenticationSession
集成此库目前不具备 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.Configuration
和 URLResponseProvider
控制。默认情况下,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
...
}
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)
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)
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 有一个 复杂 的 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,而不是因为空格而犹豫不决。
参与此项目即表示您同意遵守贡献者行为准则。