Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4beed82
Add models and method signatures
Widcket Apr 23, 2025
f7d1b1a
Add overloads with default values
Widcket Apr 24, 2025
8217fc1
Add `passkeySignupChallenge` implementation
Widcket Apr 24, 2025
3cb80ef
Implement the login method
Widcket Apr 24, 2025
96a4f91
Simplify login API by pre-encoding/decoding `Data` instances
Widcket Apr 25, 2025
2bee055
Fix overloads
Widcket Apr 25, 2025
dbf95ff
Add test matchers and responses
Widcket Apr 25, 2025
a24d079
Remove overloads
Widcket Apr 26, 2025
af21b36
Simplify `SignupPasskey`
Widcket Apr 27, 2025
27c5fc9
Simplify challenge model's inner types
Widcket Apr 27, 2025
120ed2e
Simplify signup models
Widcket Apr 27, 2025
913a733
Add tests for `PasskeySignupChallenge`
Widcket Apr 27, 2025
051c7e8
Add API docs
Widcket Apr 28, 2025
65716a0
Add documentation to EXAMPLES.md
Widcket Apr 28, 2025
254628f
Fix API docs of signup method.
Widcket Apr 28, 2025
ced6d15
Merge branch 'master' into feature/passkeys-signup
Widcket Apr 28, 2025
d4e1fcf
Add API docs to models
Widcket Apr 28, 2025
70faae2
Merge branch 'feature/passkeys-signup' of github.com:auth0/Auth0.swif…
Widcket Apr 28, 2025
e910229
Update `@available` checks with the correct macOS version
Widcket Apr 28, 2025
25d8ed1
Update `#available` checks with the correct macOS version
Widcket Apr 28, 2025
a97c943
Exclude tvOS along watchOS
Widcket Apr 28, 2025
ab6a75b
Remove specific files from tvOS and watchOS targets
Widcket Apr 28, 2025
ed7967e
Add missing `@available` attributes
Widcket Apr 28, 2025
06e28b9
Add missing `@available` attribute in test code
Widcket Apr 28, 2025
3cf2003
Add missing `@available` attribute to test class
Widcket Apr 28, 2025
dff5192
Exclude test file from tvOS target
Widcket Apr 28, 2025
3d82798
Move files to the base `Auth0` directory
Widcket Apr 29, 2025
fbfa8c0
Update mark comment in test file
Widcket Apr 29, 2025
7a8b140
Remove unused test helper function
Widcket Apr 29, 2025
079910c
Update code snippets in EXAMPLES.ms
Widcket Apr 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 56 additions & 16 deletions Auth0.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions Auth0/Auth0Authentication.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// swiftlint:disable file_length
// swiftlint:disable type_body_length

import Foundation

struct Auth0Authentication: Authentication {
Expand Down Expand Up @@ -205,6 +208,78 @@ struct Auth0Authentication: Authentication {
telemetry: self.telemetry)
}

#if !os(tvOS) && !os(watchOS)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the implementation of the login method.

@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func login(signupPasskey attestation: SignupPasskey,
signupChallenge challenge: PasskeySignupChallenge,
connection: String?,
audience: String?,
scope: String) -> Request<Credentials, AuthenticationError> {
let url = URL(string: "oauth/token", relativeTo: self.url)!
let id = attestation.credentialID.encodeBase64URLSafe()

var authenticatorResponse: [String: Any] = [
"id": id,
"rawId": id,
"type": "public-key",
"response": [
"clientDataJSON": attestation.rawClientDataJSON.encodeBase64URLSafe(),
"attestationObject": attestation.rawAttestationObject!.encodeBase64URLSafe()
]
]

authenticatorResponse["authenticatorAttachment"] = attestation.attachment.stringValue

var payload: [String: Any] = [
"client_id": self.clientId,
"grant_type": "urn:okta:params:oauth:grant-type:webauthn",
"auth_session": challenge.authenticationSession,
"authn_response": authenticatorResponse
]

payload["realm"] = connection
payload["audience"] = audience
payload["scope"] = includeRequiredScope(in: scope)

return Request(session: session,
url: url,
method: "POST",
handle: codable,
parameters: payload,
logger: self.logger,
telemetry: self.telemetry)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the implementation of the request challenge method.

@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func passkeySignupChallenge(email: String?,
phoneNumber: String?,
username: String?,
name: String?,
connection: String?) -> Request<PasskeySignupChallenge, AuthenticationError> {
let url = URL(string: "passkey/register", relativeTo: self.url)!

var userProfile: [String: Any] = [:]
userProfile["email"] = email
userProfile["phone_number"] = phoneNumber
userProfile["username"] = username
userProfile["name"] = name

var payload: [String: Any] = [
"client_id": self.clientId,
"user_profile": userProfile
]
payload["realm"] = connection

return Request(session: session,
url: url,
method: "POST",
handle: codable,
parameters: payload,
logger: self.logger,
telemetry: self.telemetry)
}
#endif

func resetPassword(email: String, connection: String) -> Request<Void, AuthenticationError> {
let payload = [
"email": email,
Expand Down
189 changes: 176 additions & 13 deletions Auth0/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public protocol Authentication: Trackable, Loggable {
// MARK: - Methods

/**
Logs in a user using an email and an OTP code received via email. This is the last part of the passwordless login
Logs a user in using an email and an OTP code received via email. This is the last part of the passwordless login
flow.

## Usage
Expand Down Expand Up @@ -70,7 +70,7 @@ public protocol Authentication: Trackable, Loggable {
func login(email: String, code: String, audience: String?, scope: String) -> Request<Credentials, AuthenticationError>

/**
Logs in a user using a phone number and an OTP code received via SMS. This is the last part of the passwordless login flow.
Logs a user in using a phone number and an OTP code received via SMS. This is the last part of the passwordless login flow.

## Usage

Expand Down Expand Up @@ -116,7 +116,7 @@ public protocol Authentication: Trackable, Loggable {
func login(phoneNumber: String, code: String, audience: String?, scope: String) -> Request<Credentials, AuthenticationError>

/**
Logs in a user using a username and password with a realm or connection.
Logs a user in using a username and password with a realm or connection.

## Usage

Expand Down Expand Up @@ -150,11 +150,11 @@ public protocol Authentication: Trackable, Loggable {
```

- Parameters:
- usernameOrEmail: Username or email of the user.
- password: Password of the user.
- realmOrConnection: Domain of the realm or connection name.
- audience: API Identifier that your application is requesting access to.
- scope: Space-separated list of requested scope values.
- username: Username or email of the user.
- password: Password of the user.
- realm: Domain of the realm or connection name.
- audience: API Identifier that your application is requesting access to.
- scope: Space-separated list of requested scope values.
- Returns: Request that will yield Auth0 user's credentials.
- Requires: The `http://auth0.com/oauth/grant-type/password-realm` grant. Check
[our documentation](https://auth0.com/docs/get-started/applications/application-grant-types) for more information.
Expand Down Expand Up @@ -299,7 +299,7 @@ public protocol Authentication: Trackable, Loggable {
func multifactorChallenge(mfaToken: String, types: [String]?, authenticatorId: String?) -> Request<Challenge, AuthenticationError>

/**
Authenticates a user with their Sign In with Apple authorization code.
Logs a user in with their Sign In with Apple authorization code.

## Usage

Expand Down Expand Up @@ -344,7 +344,7 @@ public protocol Authentication: Trackable, Loggable {
func login(appleAuthorizationCode authorizationCode: String, fullName: PersonNameComponents?, profile: [String: Any]?, audience: String?, scope: String) -> Request<Credentials, AuthenticationError>

/**
Authenticates a user with their Facebook [session info access token](https://developers.facebook.com/docs/facebook-login/access-tokens/session-info-access-token/) and profile data.
Logs a user in with their Facebook [session info access token](https://developers.facebook.com/docs/facebook-login/access-tokens/session-info-access-token/) and profile data.

## Usage

Expand Down Expand Up @@ -389,7 +389,7 @@ public protocol Authentication: Trackable, Loggable {
func login(facebookSessionAccessToken sessionAccessToken: String, profile: [String: Any], audience: String?, scope: String) -> Request<Credentials, AuthenticationError>

/**
Logs in a user using a username and password in the default directory.
Logs a user in using a username and password in the default directory.

## Usage

Expand Down Expand Up @@ -482,7 +482,7 @@ public protocol Authentication: Trackable, Loggable {
- email: Email for the new user.
- username: Username for the new user (if the connection requires a username). Defaults to `nil`.
- password: Password for the new user.
- connection: Name of the connection where the user will be created (database connection).
- connection: Name of the database connection where the user will be created.
- userMetadata: Additional user metadata parameters that will be added to the newly created user.
- rootAttributes: Root attributes that will be added to the newly created user. These will not overwrite existing parameters. See https://auth0.com/docs/api/authentication#signup for the full list of supported attributes.
- Returns: A request that will yield a newly created database user (just the email, username, and email verified flag).
Expand All @@ -493,6 +493,141 @@ public protocol Authentication: Trackable, Loggable {
*/
func signup(email: String, username: String?, password: String, connection: String, userMetadata: [String: Any]?, rootAttributes: [String: Any]?) -> Request<DatabaseUser, AuthenticationError>

#if !os(tvOS) && !os(watchOS)
/// Logs a user in using a signup passkey credential and the signup challenge. This is the last part of the passkey signup flow.
///
/// ## Availability
///
/// This feature is currently available in
/// [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access).
/// Please reach out to Auth0 support to get it enabled for your tenant.
///
/// ## Usage
///
/// ```swift
/// Auth0
/// .authentication()
/// .login(signupPasskey: signupPasskey,
/// signupChallenge: signupChallenge,
/// connection: "Username-Password-Authentication")
/// .start { result in
/// switch result {
/// case .success(let credentials):
/// print("Obtained credentials: \(credentials)")
/// case .failure(let error):
/// print("Failed with: \(error)")
/// }
/// }
/// ```
///
/// You can also specify audience (the Auth0 API identifier) and scope values:
///
/// ```swift
/// Auth0
/// .authentication()
/// .login(signupPasskey: signupPasskey,
/// signupChallenge: signupChallenge,
/// connection: "Username-Password-Authentication",
/// audience: "https://example.com/api",
/// scope: "openid profile email offline_access")
/// .start { print($0) }
/// ```
///
/// - Parameters:
/// - attestation: The signup passkey credential obtained from the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate.
/// - challenge: The passkey signup challenge obtained from ``passkeySignupChallenge(email:phoneNumber:username:name:connection:)``.
/// - connection: Name of the database connection where the user will be created. If a connection name is not specified, your tenant's default directory will be used.
/// - audience: API Identifier that your application is requesting access to.
/// - scope: Space-separated list of requested scope values.
/// - Returns: A request that will yield Auth0 user's credentials.
///
/// - [Authentication API Endpoint](https://auth0.com/docs/native-passkeys-api#authenticate-new-user)
/// - [Native Passkeys for Mobile Applications](https://auth0.com/docs/native-passkeys-for-mobile-applications)
/// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service)
@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func login(signupPasskey attestation: SignupPasskey,
signupChallenge challenge: PasskeySignupChallenge,
connection: String?,
audience: String?,
scope: String) -> Request<Credentials, AuthenticationError>

/// Requests a challenge for registering a new user with a passkey. This is the first part of the passkey signup flow.
///
/// You need to provide at least one user identifier and an optional display `name`. By default, database
/// connections require a valid `email`. If you have enabled [Flexible Identifiers](https://auth0.com/docs/authenticate/database-connections/activate-and-configure-attributes-for-flexible-identifiers)
/// for your database connection, you may use any combination of `email`, `phoneNumber`, or `username`. These
/// options can be required or optional and must match your Flexible Identifiers configuration.
///
/// ## Availability
///
/// This feature is currently available in
/// [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access).
/// Please reach out to Auth0 support to get it enabled for your tenant.
///
/// ## Usage
///
/// ```swift
/// Auth0
/// .authentication()
/// .passkeySignupChallenge(email: "support@auth0.com",
/// name: "John Appleseed",
/// connection: "Username-Password-Authentication")
/// .start { result in
/// switch result {
/// case .success(let signupChallenge):
/// print("Obtained signup challenge: \(signupChallenge)")
/// case .failure(let error):
/// print("Failed with: \(error)")
/// }
/// }
/// ```
///
/// Use the challenge with [`ASAuthorizationPlatformPublicKeyCredentialProvider`](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialprovider)
/// from the `AuthenticationServices` framework to generate a new passkey credential. It will be delivered through the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate)
/// delegate. Check out [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service)
/// to learn more.
///
/// ```swift
/// let credentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
/// relyingPartyIdentifier: signupChallenge.relyingPartyId
/// )
///
/// let registrationRequest = credentialProvider.createCredentialRegistrationRequest(
/// challenge: signupChallenge.challengeData,
/// name: signupChallenge.userName,
/// userID: signupChallenge.userId
/// )
///
/// let authController = ASAuthorizationController(authorizationRequests: [registrationRequest])
/// authController.delegate = self // ASAuthorizationControllerDelegate
/// authController.presentationContextProvider = self
/// authController.performRequests()
/// ```
///
/// Then, call ``login(signupPasskey:signupChallenge:connection:audience:scope:)`` with the created
/// passkey credential and the challenge to log the new user in.
///
/// - Parameters:
/// - email: Email address of the user.
/// - phoneNumber: Phone number of the user.
/// - username: Username of the user.
/// - name: Display name of the user.
/// - connection: Name of the database connection where the user will be created. If a connection name is not specified, your tenant's default directory will be used.
/// - Returns: A request that will yield a passkey signup challenge.
///
/// ## See Also
///
/// - [Authentication API Endpoint](https://auth0.com/docs/native-passkeys-api#request-signup-challenge)
/// - [Native Passkeys for Mobile Applications](https://auth0.com/docs/native-passkeys-for-mobile-applications)
/// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service)
@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func passkeySignupChallenge(email: String?,
phoneNumber: String?,
username: String?,
name: String?,
connection: String?) -> Request<PasskeySignupChallenge, AuthenticationError>
#endif

/**
Resets the password of a database user.

Expand Down Expand Up @@ -712,7 +847,7 @@ public protocol Authentication: Trackable, Loggable {
- [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token)
- [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
*/
func ssoExchange(withRefreshToken: String) -> Request<SSOCredentials, AuthenticationError>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to address a DoCC warning:

Screenshot 2025-04-28 at 12 56 12

Since this is the internal parameter (the external one is withRefreshToken), it has no bearing on the public API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshToken is already the internal parameter in the actual implementing type.

func ssoExchange(withRefreshToken refreshToken: String) -> Request<SSOCredentials, AuthenticationError>

/**
Renews the user's credentials using a refresh token.
Expand Down Expand Up @@ -845,6 +980,34 @@ public extension Authentication {
return self.signup(email: email, username: username, password: password, connection: connection, userMetadata: userMetadata, rootAttributes: rootAttributes)
}

#if !os(tvOS) && !os(watchOS)
@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func login(signupPasskey attestation: SignupPasskey,
signupChallenge challenge: PasskeySignupChallenge,
connection: String? = nil,
audience: String? = nil,
scope: String = defaultScope) -> Request<Credentials, AuthenticationError> {
self.login(signupPasskey: attestation,
signupChallenge: challenge,
connection: connection,
audience: audience,
scope: scope)
}

@available(iOS 16.6, macOS 13.5, visionOS 1.0, *)
func passkeySignupChallenge(email: String? = nil,
phoneNumber: String? = nil,
username: String? = nil,
name: String? = nil,
connection: String? = nil) -> Request<PasskeySignupChallenge, AuthenticationError> {
return self.passkeySignupChallenge(email: email,
phoneNumber: phoneNumber,
username: username,
name: name,
connection: connection)
}
#endif

func startPasswordless(email: String, type: PasswordlessType = .code, connection: String = "email") -> Request<Void, AuthenticationError> {
return self.startPasswordless(email: email, type: type, connection: connection)
}
Expand Down
1 change: 0 additions & 1 deletion Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,6 @@ public extension CredentialsManager {
/// - Parameters:
/// - parameters: Additional parameters to use.
/// - headers: Additional headers to use.
/// - callback: Callback that receives a `Result` containing either the renewed user's credentials or an error.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async/await wrapper has no callback.

/// - Requires: The scope `offline_access` to have been requested on login to get a refresh token from Auth0. If
/// there is no refresh token, a ``CredentialsManagerError/noRefreshToken`` error will be thrown.
/// - Warning: Do not call `store(credentials:)` afterward. The Credentials Manager automatically persists the
Expand Down
3 changes: 2 additions & 1 deletion Auth0/Handlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func plainJson(from response: Response<AuthenticationError>, callback: Request<[
}
}

func codable<T: Codable>(from response: Response<AuthenticationError>, callback: Request<T, AuthenticationError>.Callback) {
func codable<T: Decodable>(from response: Response<AuthenticationError>,
callback: Request<T, AuthenticationError>.Callback) {
do {
if let dictionary = try response.result() as? [String: Any] {
let data = try JSONSerialization.data(withJSONObject: dictionary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public struct Challenge: Codable, Sendable {
/// How the user will get the challenge and prove possession.
public let challengeType: String

/// Out-of-Band (OOB) code.
/// Out-of-band (OOB) code.
public let oobCode: String?

/// When the challenge response includes a `prompt` binding method, your app needs to prompt the user for the
Expand Down
8 changes: 8 additions & 0 deletions Auth0/NSData+URLSafe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ public extension Data {

/// Encodes data to base64url.
func a0_encodeBase64URLSafe() -> String? {
return self.encodeBase64URLSafe()
}

}

extension Data {

func encodeBase64URLSafe() -> String {
return self
.base64EncodedString(options: [])
.replacingOccurrences(of: "+", with: "-")
Expand Down
Loading
Loading