Skip to content

Commit 9a73fe6

Browse files
authored
Add support for Multi-Resource Refresh Token (MRRT) (#912)
1 parent 5680abb commit 9a73fe6

17 files changed

+1696
-320
lines changed

Auth0.xcodeproj/project.pbxproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,15 @@
201201
5CF539292836FB0C0073F623 /* ClearSessionTransactionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF539272836FB0C0073F623 /* ClearSessionTransactionSpec.swift */; };
202202
5CF5392B283835470073F623 /* ASProviderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF5392A283835460073F623 /* ASProviderSpec.swift */; };
203203
5CF5392C283835470073F623 /* ASProviderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF5392A283835460073F623 /* ASProviderSpec.swift */; };
204+
5CFB82502D5BF324009FD237 /* APICredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */; };
205+
5CFB82512D5BF324009FD237 /* APICredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */; };
206+
5CFB82522D5BF324009FD237 /* APICredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */; };
207+
5CFB82532D5BF324009FD237 /* APICredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */; };
208+
5CFB82542D5BF324009FD237 /* APICredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */; };
209+
5CFB82562D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */; };
210+
5CFB82572D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */; };
211+
5CFB82582D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */; };
212+
5CFB82592D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */; };
204213
5CFB82612D6D221F009FD237 /* Barrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82602D6D221C009FD237 /* Barrier.swift */; };
205214
5CFB82622D6D221F009FD237 /* Barrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82602D6D221C009FD237 /* Barrier.swift */; };
206215
5CFB82632D6D221F009FD237 /* Barrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFB82602D6D221C009FD237 /* Barrier.swift */; };
@@ -722,6 +731,8 @@
722731
5CF539232836DCC10073F623 /* SafariProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariProviderSpec.swift; sourceTree = "<group>"; };
723732
5CF539272836FB0C0073F623 /* ClearSessionTransactionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearSessionTransactionSpec.swift; sourceTree = "<group>"; };
724733
5CF5392A283835460073F623 /* ASProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASProviderSpec.swift; sourceTree = "<group>"; };
734+
5CFB824F2D5BF31D009FD237 /* APICredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICredentials.swift; sourceTree = "<group>"; };
735+
5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICredentialsSpec.swift; sourceTree = "<group>"; };
725736
5CFB82602D6D221C009FD237 /* Barrier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Barrier.swift; sourceTree = "<group>"; };
726737
5CFB826F2D6E640B009FD237 /* SSOCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOCredentials.swift; sourceTree = "<group>"; };
727738
5CFB82752D6FD287009FD237 /* SSOCredentialsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOCredentialsSpec.swift; sourceTree = "<group>"; };
@@ -1248,6 +1259,7 @@
12481259
children = (
12491260
5FBBF0421CCA90300024D2AF /* AuthenticationSpec.swift */,
12501261
5FE2F8A51CCA9C17003628F4 /* CredentialsSpec.swift */,
1262+
5CFB82552D5E9F94009FD237 /* APICredentialsSpec.swift */,
12511263
5CFB82752D6FD287009FD237 /* SSOCredentialsSpec.swift */,
12521264
5B2860D41EEF20F300C75D54 /* UserInfoSpec.swift */,
12531265
5C3D88672DC2CCA100AACC34 /* PasskeyLoginChallenge.swift */,
@@ -1320,6 +1332,7 @@
13201332
5C4F552223C8FBA100C89615 /* JWKS.swift */,
13211333
5B2860CD1EEAC30500C75D54 /* UserInfo.swift */,
13221334
5FDE874E1D8A424700EA27DC /* Credentials.swift */,
1335+
5CFB824F2D5BF31D009FD237 /* APICredentials.swift */,
13231336
5CFB826F2D6E640B009FD237 /* SSOCredentials.swift */,
13241337
5FDE874F1D8A424700EA27DC /* Handlers.swift */,
13251338
);
@@ -2126,6 +2139,7 @@
21262139
5F06DDC91CC66B710011842B /* Auth0.swift in Sources */,
21272140
5C3D87E92DB99C5C00AACC34 /* PasskeySignupChallenge.swift in Sources */,
21282141
5B1748741EF2D3A40060E653 /* Helpers.swift in Sources */,
2142+
5CFB82542D5BF324009FD237 /* APICredentials.swift in Sources */,
21292143
5FDE87691D8A424700EA27DC /* Credentials.swift in Sources */,
21302144
5FE2F8B21CCEAED8003628F4 /* Requestable.swift in Sources */,
21312145
5C4F551E23C8FB8E00C89615 /* Array+Encode.swift in Sources */,
@@ -2163,6 +2177,7 @@
21632177
5C0AF09E2833420200162044 /* WebAuthentication.swift in Sources */,
21642178
5FDE876A1D8A424700EA27DC /* Credentials.swift in Sources */,
21652179
5C354C05276CE1A500ADBC86 /* PasswordlessType.swift in Sources */,
2180+
5CFB82512D5BF324009FD237 /* APICredentials.swift in Sources */,
21662181
5C3D88262DC1509800AACC34 /* LoginPasskey.swift in Sources */,
21672182
5F28B4621D8216180000EB23 /* Loggable.swift in Sources */,
21682183
5C41F6D4244F974100252548 /* OAuth2Grant.swift in Sources */,
@@ -2246,6 +2261,7 @@
22462261
5FADB60F1CED7E5200D4BB50 /* UserPatchAttributesSpec.swift in Sources */,
22472262
5C3D880E2DBE7F3D00AACC34 /* PasskeySignupChallengeSpec.swift in Sources */,
22482263
5C4F553523C9124200C89615 /* JWKSpec.swift in Sources */,
2264+
5CFB82592D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */,
22492265
5FA250541D4A85A200C544FA /* WebAuthErrorSpec.swift in Sources */,
22502266
5CB41D7623D0C15000074024 /* IDTokenValidatorBaseSpec.swift in Sources */,
22512267
5B9262C31ECF0CC200F4F6D3 /* BioAuthenticationSpec.swift in Sources */,
@@ -2304,6 +2320,7 @@
23042320
C177D7762C2BE00D0094C657 /* StubURLProtocol.swift in Sources */,
23052321
5CE775AF244FD66D00D054A0 /* OAuth2GrantSpec.swift in Sources */,
23062322
5FD255B21D14A9E000387ECB /* AuthenticationErrorSpec.swift in Sources */,
2323+
5CFB82582D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */,
23072324
5C53A7E92703A23200A7C0A3 /* UserInfoSpec.swift in Sources */,
23082325
C177D7712C2BDFE40094C657 /* NetworkStub.swift in Sources */,
23092326
5CF539252836DCC10073F623 /* SafariProviderSpec.swift in Sources */,
@@ -2355,6 +2372,7 @@
23552372
5CFB82712D6E640F009FD237 /* SSOCredentials.swift in Sources */,
23562373
5F23E6E41D4ACD8500C3F2D9 /* JSONObjectPayload.swift in Sources */,
23572374
5C4F551C23C8FB8E00C89615 /* String+URLSafe.swift in Sources */,
2375+
5CFB82502D5BF324009FD237 /* APICredentials.swift in Sources */,
23582376
5F23E6DC1D4ACD6100C3F2D9 /* NSData+URLSafe.swift in Sources */,
23592377
5F23E6E61D4ACD8500C3F2D9 /* Requestable.swift in Sources */,
23602378
5C354C07276CE1A500ADBC86 /* PasswordlessType.swift in Sources */,
@@ -2395,6 +2413,7 @@
23952413
5CFB82722D6E640F009FD237 /* SSOCredentials.swift in Sources */,
23962414
5C4F552623C8FBA100C89615 /* JWKS.swift in Sources */,
23972415
5B0893E620F8A52100FBF962 /* CredentialsManager.swift in Sources */,
2416+
5CFB82532D5BF324009FD237 /* APICredentials.swift in Sources */,
23982417
5F23E7061D4B88EA00C3F2D9 /* NSData+URLSafe.swift in Sources */,
23992418
5F23E70F1D4B88FC00C3F2D9 /* Requestable.swift in Sources */,
24002419
5C354C06276CE1A500ADBC86 /* PasswordlessType.swift in Sources */,
@@ -2420,6 +2439,7 @@
24202439
5F28B4691D8300D50000EB23 /* LoggerSpec.swift in Sources */,
24212440
5F1FBB9A1D8A44C0006B0B85 /* ResponseSpec.swift in Sources */,
24222441
5F331B0C1D4BB7F900AE4382 /* UsersSpec.swift in Sources */,
2442+
5CFB82572D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */,
24232443
C12BFE442C352DD700D1CC00 /* StubURLProtocol.swift in Sources */,
24242444
5F331B0E1D4BB80700AE4382 /* Matchers.swift in Sources */,
24252445
5F331B0A1D4BB7F900AE4382 /* ManagementSpec.swift in Sources */,
@@ -2464,6 +2484,7 @@
24642484
5C3D87E42DB8276000AACC34 /* PublicKeyCredentialCreationOptions.swift in Sources */,
24652485
5C3D881D2DC148C600AACC34 /* PasskeyLoginChallenge.swift in Sources */,
24662486
C1B3B9F32C24B6D4004A32A4 /* JWKS.swift in Sources */,
2487+
5CFB82522D5BF324009FD237 /* APICredentials.swift in Sources */,
24672488
C1B3B9F42C24B6D4004A32A4 /* UserInfo.swift in Sources */,
24682489
C1B3B9F52C24B6D4004A32A4 /* Credentials.swift in Sources */,
24692490
C1B3B9F62C24B6D4004A32A4 /* Handlers.swift in Sources */,
@@ -2555,6 +2576,7 @@
25552576
C1B3BA452C24BA36004A32A4 /* IDTokenValidatorMocks.swift in Sources */,
25562577
C1B3BA462C24BA36004A32A4 /* IDTokenValidatorSpec.swift in Sources */,
25572578
C1B3BA472C24BA36004A32A4 /* IDTokenSignatureValidatorSpec.swift in Sources */,
2579+
5CFB82562D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */,
25582580
C1B3BA482C24BA36004A32A4 /* ClaimValidatorsSpec.swift in Sources */,
25592581
C1B3BA492C24BA37004A32A4 /* BioAuthenticationSpec.swift in Sources */,
25602582
5C3D886B2DC2CCA200AACC34 /* PasskeyLoginChallenge.swift in Sources */,

Auth0/APICredentials.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import Foundation
2+
3+
private struct _A0APICredentials {
4+
let accessToken: String
5+
let tokenType: String
6+
let expiresIn: Date
7+
let scope: String
8+
}
9+
10+
/// User's credentials obtained from Auth0 for a specific API as the result of exchanging a refresh token.
11+
public struct APICredentials: CustomStringConvertible {
12+
13+
/// Token that can be used to make authenticated requests to the API.
14+
///
15+
/// ## See Also
16+
///
17+
/// - [Access Tokens](https://auth0.com/docs/secure/tokens/access-tokens)
18+
public let accessToken: String
19+
20+
/// Indicates how the access token should be used. For example, as a bearer token.
21+
public let tokenType: String
22+
23+
/// When the access token expires.
24+
public let expiresIn: Date
25+
26+
/// The scopes that have been granted by Auth0.
27+
///
28+
/// ## See Also
29+
///
30+
/// - [Scopes](https://auth0.com/docs/get-started/apis/scopes)
31+
public let scope: String
32+
33+
/// Custom description that redacts the access token with `<REDACTED>`.
34+
public var description: String {
35+
let redacted = "<REDACTED>"
36+
let values = _A0APICredentials(accessToken: redacted,
37+
tokenType: self.tokenType,
38+
expiresIn: self.expiresIn,
39+
scope: self.scope)
40+
return String(describing: values).replacingOccurrences(of: "_A0APICredentials", with: "APICredentials")
41+
}
42+
43+
// MARK: - Initializer
44+
45+
/// Default initializer.
46+
public init(accessToken: String,
47+
tokenType: String,
48+
expiresIn: Date,
49+
scope: String) {
50+
self.accessToken = accessToken
51+
self.tokenType = tokenType
52+
self.expiresIn = expiresIn
53+
self.scope = scope
54+
}
55+
}
56+
57+
// MARK: - Codable
58+
59+
extension APICredentials: Codable {
60+
61+
enum CodingKeys: String, CodingKey {
62+
case accessToken = "access_token"
63+
case tokenType = "token_type"
64+
case expiresIn = "expires_in"
65+
case scope
66+
}
67+
68+
private static let jsonEncoder: JSONEncoder = {
69+
let encoder = JSONEncoder()
70+
encoder.dateEncodingStrategy = .secondsSince1970
71+
return encoder
72+
}()
73+
74+
private static let jsonDecoder: JSONDecoder = {
75+
let decoder = JSONDecoder()
76+
decoder.dateDecodingStrategy = .secondsSince1970
77+
return decoder
78+
}()
79+
80+
internal func encode() throws -> Data {
81+
return try Self.jsonEncoder.encode(self)
82+
}
83+
84+
internal init(from data: Data) throws {
85+
self = try Self.jsonDecoder.decode(Self.self, from: data)
86+
}
87+
88+
}
89+
90+
// MARK: - Internal Initializer
91+
92+
extension APICredentials {
93+
94+
init(from credentials: Credentials) {
95+
self.accessToken = credentials.accessToken
96+
self.tokenType = credentials.tokenType
97+
self.expiresIn = credentials.expiresIn
98+
self.scope = credentials.scope ?? ""
99+
}
100+
101+
}

Auth0/Auth0Authentication.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,19 +418,28 @@ struct Auth0Authentication: Authentication {
418418
])
419419
}
420420

421-
func renew(withRefreshToken refreshToken: String, scope: String? = nil) -> Request<Credentials, AuthenticationError> {
421+
func renew(withRefreshToken refreshToken: String, audience: String? = nil, scope: String? = nil) -> Request<Credentials, AuthenticationError> {
422422
var payload: [String: Any] = [
423423
"refresh_token": refreshToken,
424424
"grant_type": "refresh_token",
425425
"client_id": self.clientId
426426
]
427-
payload["scope"] = scope
428427
let oauthToken = URL(string: "oauth/token", relativeTo: self.url)!
428+
429+
if let audience = audience {
430+
// Make sure to always include the 'openid' scope if we're trying to get a new set of credentials for an
431+
// API like My Account API. This way, we'll always get an ID token, matching the existing login behavior.
432+
payload["scope"] = includeRequiredScope(in: scope)
433+
payload["audience"] = audience
434+
} else {
435+
payload["scope"] = scope
436+
}
437+
429438
return Request(session: session,
430439
url: oauthToken,
431440
method: "POST",
432441
handle: codable,
433-
parameters: payload, // Initializer does not enforce 'openid' scope
442+
parameters: payload,
434443
logger: self.logger,
435444
telemetry: self.telemetry)
436445
}

Auth0/Authentication.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -987,19 +987,22 @@ public protocol Authentication: Trackable, Loggable {
987987
}
988988
```
989989

990-
You can get a downscoped access token by requesting fewer scopes than were originally requested on login:
990+
You can request credentials for a specific API by passing its audience value. The default scopes configured for
991+
the API will be granted if you don't request any specific scopes.
991992

992993
```swift
993994
Auth0
994995
.authentication()
995996
.renew(withRefreshToken: credentials.refreshToken,
996-
scope: "openid offline_access")
997+
audience: "http://example.com/api",
998+
scope: "read:todos update:todos")
997999
.start { print($0) }
9981000
```
9991001

10001002
- Parameters:
10011003
- refreshToken: The refresh token.
1002-
- scope: Space-separated list of scope values to request. Defaults to `nil`, which will ask for the same scopes that were originally requested on login.
1004+
- audience: Identifier of the API that your application is requesting access to. Defaults to `nil`.
1005+
- scope: Space-separated list of scope values to request. Defaults to `nil`.
10031006
- Returns: A request that will yield Auth0 user's credentials.
10041007

10051008
## See Also
@@ -1008,7 +1011,7 @@ public protocol Authentication: Trackable, Loggable {
10081011
- [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens)
10091012
- <doc:RefreshTokens>
10101013
*/
1011-
func renew(withRefreshToken refreshToken: String, scope: String?) -> Request<Credentials, AuthenticationError>
1014+
func renew(withRefreshToken refreshToken: String, audience: String?, scope: String?) -> Request<Credentials, AuthenticationError>
10121015

10131016
/**
10141017
Revokes a user's refresh token by performing a request to the `/oauth/revoke` endpoint.
@@ -1153,8 +1156,8 @@ public extension Authentication {
11531156
return self.startPasswordless(phoneNumber: phoneNumber, type: type, connection: connection)
11541157
}
11551158

1156-
func renew(withRefreshToken refreshToken: String, scope: String? = nil) -> Request<Credentials, AuthenticationError> {
1157-
return self.renew(withRefreshToken: refreshToken, scope: scope)
1159+
func renew(withRefreshToken refreshToken: String, audience: String? = nil, scope: String? = nil) -> Request<Credentials, AuthenticationError> {
1160+
return self.renew(withRefreshToken: refreshToken, audience: audience, scope: scope)
11581161
}
11591162

11601163
}

Auth0/Credentials.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
private struct _StructCredentials {
3+
private struct _A0Credentials {
44
let accessToken: String
55
let tokenType: String
66
let idToken: String
@@ -72,14 +72,14 @@ public final class Credentials: NSObject, Sendable {
7272
/// Custom description that redacts the tokens with `<REDACTED>`.
7373
public override var description: String {
7474
let redacted = "<REDACTED>"
75-
let values = _StructCredentials(accessToken: redacted,
76-
tokenType: self.tokenType,
77-
idToken: redacted,
78-
refreshToken: (self.refreshToken != nil) ? redacted : nil,
79-
expiresIn: self.expiresIn,
80-
scope: self.scope,
81-
recoveryCode: (self.recoveryCode != nil) ? redacted : nil)
82-
return String(describing: values).replacingOccurrences(of: "_StructCredentials", with: "Credentials")
75+
let values = _A0Credentials(accessToken: redacted,
76+
tokenType: self.tokenType,
77+
idToken: redacted,
78+
refreshToken: (self.refreshToken != nil) ? redacted : nil,
79+
expiresIn: self.expiresIn,
80+
scope: self.scope,
81+
recoveryCode: (self.recoveryCode != nil) ? redacted : nil)
82+
return String(describing: values).replacingOccurrences(of: "_A0Credentials", with: "Credentials")
8383
}
8484

8585
// MARK: - Initializer

0 commit comments

Comments
 (0)