ChimeHQ/OAuthenticator
OAuth 2.1 request authentication
Integration
Swift Package Manager:
dependencies: [
.package(url: "https://github.com/ChimeHQ/OAuthenticator", from: "0.3.0")
]Usage
The main type is the `Authenticator`. It can execute a `URLRequest` in a similar fashion to `URLSession`, but will handle all authentication requirements and tack on the needed `Authorization` header. Its behavior is controlled via `Authenticator.Configuration` and `URLResponseProvider`. By default, the `URLResponseProvider` will be a private `URLSession`, but you can customize this if needed.
Setting up a `Configuration` can be more work, depending on the OAuth service you're interacting with.
```swift
// 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" },
pkce: PKCEVerifier(hash: "S256", hasher: { ... })
)
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)
```
If you want to receive the result of the authentication process without issuing a request first, you can specify
an optional `Authenticator.AuthenticationStatusHandler` callback function within the `Authenticator.Configuration` initializer.
This allows you to support special cases where you need to capture the `Login` object before executing your first
authenticated `URLRequest` and manage that separately.
``` swift
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
...
}
```
### DPoP
Constructing and signing the JSON Web Token / JSON Web Keys necessary for DPoP suppot is mostly out of the scope of this library. But here's an example of how to do it, using [Jot](https://github.com/mattmassicotte/Jot), a really basic JWT/JWK library I put together. You should be able to use this as a guide if you want to use a different JWT/JWK library.
```swift
import Jot
import OAuthenticator
// generate a DPoP key
let key = DPoPKey.P256()
// define your claims, making sure to pay attention to the JSON coding keys
struct DPoPTokenClaims : JSONWebTokenPayload {
// standard claims
let iss: String?
let jti: String?
let iat: Date?
let exp: Date?
// custom claims, which could vary depending on the service you are working with
let htm: String?
let htu: String?
}
// produce a DPoPSigner.JWTGenerator function from that key
extension DPoPSigner {
static func JSONWebTokenGenerator(dpopKey: DPoPKey) -> DPoPSigner.JWTGenerator {
let id = dpopKey.id.uuidString
return { params in
// construct the private key
let key = try dpopKey.p256PrivateKey
// make the JWK
let jwk = JSONWebKey(p256Key: key.publicKey)
// fill in all the JWT fields, including whatever custom claims you need
let newToken = JSONWebToken<DPoPTokenClaims>(
header: JSONWebTokenHeader(
algorithm: .ES256,
type: params.keyType,
keyId: id,
jwk: jwk
),
payload: DPoPTokenClaims(
iss: params.issuingServer,
htm: params.httpMethod,
htu: params.requestEndpoint
)
)
return try newToken.encode(with: key)
}
}
}
```
### GitHub
OAuthenticator also comes with pre-packaged configuration for GitHub, which makes set up much more straight-forward.
```swift
// 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 also comes with pre-packaged configuration for Mastodon, which makes set up much more straight-forward.
For more info, please check out [https://docs.joinmastodon.org/client/token/](https://docs.joinmastodon.org/client/token/)
```swift
// 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 also comes with pre-packaged configuration for Google APIs (access to Google Drive, Google People, Google Calendar, ...) according to the application requested scopes.
More info about those at [Google Workspace](https://developers.google.com/workspace). The Google OAuth process is described in [Google Identity](https://developers.google.com/identity)
Integration example below:
```swift
// 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 has a [complex](https://docs.bsky.app/docs/advanced-guides/oauth-client) OAuth implementation.
> [!WARNING]
> bsky.social's DPoP nonce changes frequently (maybe every 10-30 seconds?). I have observed that if the nonce changes between when a user requested a 2FA code and the code being entered, the server will reject the login attempt. Trying again will involve user interaction.
Resovling PDS servers for a user is involved and beyond the scope of this library. However, [ATResolve](https://github.com/mattmassicotte/ATResolve) might help!
If you are using a platform that does not have [CryptoKit](https://developer.apple.com/documentation/cryptokit/) available, like Linux, you'll have to supply a `PKCEVerifier` parameter to the `Bluesky.tokenHandling` function.
See above for an example of how to implement DPoP JWTs.
```swift
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 generate 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,
validator: { tokenResponse, sub in
// after a token is issued, it is critical that the returned
// identity be resolved and its PDS match the issuing server
//
// check out draft-ietf-oauth-v2-1 section 7.3.1 for details
}
)
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.
```Contributing and Collaboration
I'd love to hear from you! Get in touch via an issue or pull request.
I prefer collaboration, and would love to find ways to work together if you have a similar project.
I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace.
By participating in this project you agree to abide by the Contributor Code of Conduct.
[build status]: https://github.com/ChimeHQ/OAuthenticator/actions [build status badge]: https://github.com/ChimeHQ/OAuthenticator/workflows/CI/badge.svg [platforms]: https://swiftpackageindex.com/ChimeHQ/OAuthenticator [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FOAuthenticator%2Fbadge%3Ftype%3Dplatforms [documentation]: https://swiftpackageindex.com/ChimeHQ/OAuthenticator/main/documentation [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue
Package Metadata
Repository: ChimeHQ/OAuthenticator
Stars: 99
Forks: 14
Open issues: 11
Default branch: main
Primary language: swift
License: BSD-3-Clause
Topics: ios, macos, oauth, oauth2, oauth2-client, swift
README: README.md