diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 85c58c31..9ae903ae 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -49,7 +49,21 @@ public struct CredentialsManager { self.storage = storage } - /// Enable Touch ID Authentication for additional security during credentials retrieval. + /// Retrieve the user profile from keychain synchronously, without checking if the credentials are expired + /// + /// ``` + /// let user = credentialsManager.user + /// ``` + /// - Important: Access to this property will not be protected by Biometric Authentication. + public var user: UserInfo? { + guard let credentials = retrieveCredentials(), + let idToken = credentials.idToken, + let jwt = try? decode(jwt: idToken) else { return nil } + + return UserInfo(json: jwt.body) + } + + /// Enable Biometric Authentication for additional security during credentials retrieval /// /// - Parameters: /// - title: main message to display in TouchID prompt @@ -62,7 +76,7 @@ public struct CredentialsManager { } #endif - /// Enable Biometric Authentication for additional security during credentials retrieval. + /// Enable Biometric Authentication for additional security during credentials retrieval /// /// - Parameters: /// - title: main message to display when Touch ID is used @@ -172,10 +186,16 @@ public struct CredentialsManager { } #endif - private func retrieveCredentials(withScope scope: String?, minTTL: Int, callback: @escaping (CredentialsManagerError?, Credentials?) -> Void) { + private func retrieveCredentials() -> Credentials? { guard let data = self.storage.data(forKey: self.storeKey), - let credentials = NSKeyedUnarchiver.unarchiveObject(with: data) as? Credentials else { return callback(.noCredentials, nil) } - guard let expiresIn = credentials.expiresIn else { return callback(.noCredentials, nil) } + let credentials = NSKeyedUnarchiver.unarchiveObject(with: data) as? Credentials else { return nil } + + return credentials + } + + private func retrieveCredentials(withScope scope: String?, minTTL: Int, callback: @escaping (CredentialsManagerError?, Credentials?) -> Void) { + guard let credentials = retrieveCredentials(), + let expiresIn = credentials.expiresIn else { return callback(.noCredentials, nil) } guard self.hasExpired(credentials) || self.willExpire(credentials, within: minTTL) || self.hasScopeChanged(credentials, from: scope) else { return callback(nil, credentials) } diff --git a/Auth0Tests/CredentialsManagerSpec.swift b/Auth0Tests/CredentialsManagerSpec.swift index b97fc3fd..c8379cfc 100644 --- a/Auth0Tests/CredentialsManagerSpec.swift +++ b/Auth0Tests/CredentialsManagerSpec.swift @@ -228,6 +228,36 @@ class CredentialsManagerSpec: QuickSpec { } + describe("user") { + + afterEach { + _ = credentialsManager.clear() + } + + it("should retrieve the user profile when there is an id token stored") { + let credentials = Credentials(idToken: ValidToken, expiresIn: Date(timeIntervalSinceNow: ExpiresIn)) + expect(credentialsManager.store(credentials: credentials)).to(beTrue()) + expect(credentialsManager.user).toNot(beNil()) + } + + it("should not retrieve the user profile when there are no credentials stored") { + expect(credentialsManager.user).to(beNil()) + } + + it("should not retrieve the user profile when the credentials have no id token") { + let credentials = Credentials(accessToken: AccessToken, idToken: nil, expiresIn: Date(timeIntervalSinceNow: ExpiresIn)) + expect(credentialsManager.store(credentials: credentials)).to(beTrue()) + expect(credentialsManager.user).to(beNil()) + } + + it("should not retrieve the user profile when the id token is not a jwt") { + let credentials = Credentials(accessToken: AccessToken, idToken: "not a jwt", expiresIn: Date(timeIntervalSinceNow: ExpiresIn)) + expect(credentialsManager.store(credentials: credentials)).to(beTrue()) + expect(credentialsManager.user).to(beNil()) + } + + } + describe("validity") { afterEach { diff --git a/Auth0Tests/Generators.swift b/Auth0Tests/Generators.swift index 72ad10c0..d044aa6e 100644 --- a/Auth0Tests/Generators.swift +++ b/Auth0Tests/Generators.swift @@ -25,10 +25,6 @@ import JWTDecode @testable import Auth0 -// MARK: - Constants - -fileprivate let defaultKid = "key123" - // MARK: - Keys @available(iOS 10.0, macOS 10.12, *) @@ -120,9 +116,9 @@ private func generateJWTPayload(iss: String?, @available(iOS 10.0, macOS 10.12, *) func generateJWT(alg: String = JWTAlgorithm.rs256.rawValue, - kid: String? = defaultKid, + kid: String? = Kid, iss: String? = "https://tokens-test.auth0.com/", - sub: String? = "auth0|123456789", + sub: String? = Sub, aud: [String]? = ["e31f6f9827c187e8aebdb0839a0c963a"], exp: Date? = Date().addingTimeInterval(86400000), // 1 day in milliseconds iat: Date? = Date().addingTimeInterval(-3600000), // 1 hour in milliseconds @@ -186,7 +182,7 @@ private func extractData(from bytes: UnsafePointer) -> (UnsafePointer JWK { +func generateRSAJWK(from publicKey: SecKey = TestKeys.rsaPublic, keyId: String = Kid) -> JWK { let asn = { (bytes: UnsafePointer) -> JWK? in guard bytes.pointee == 0x30 else { return nil } // Checks that this is a SEQUENCE triplet diff --git a/Auth0Tests/IDTokenSignatureValidatorSpec.swift b/Auth0Tests/IDTokenSignatureValidatorSpec.swift index 2c1c1288..0df5b617 100644 --- a/Auth0Tests/IDTokenSignatureValidatorSpec.swift +++ b/Auth0Tests/IDTokenSignatureValidatorSpec.swift @@ -97,7 +97,7 @@ class IDTokenSignatureValidatorSpec: IDTokenValidatorBaseSpec { context("kid validation") { let jwt = generateJWT() - let expectedError = IDTokenSignatureValidator.ValidationError.missingPublicKey(kid: "key123") + let expectedError = IDTokenSignatureValidator.ValidationError.missingPublicKey(kid: Kid) it("should fail if the jwk has no kid") { stub(condition: isJWKSPath(domain)) { _ in jwksResponse(kid: nil) } diff --git a/Auth0Tests/Responses.swift b/Auth0Tests/Responses.swift index f73cb8f4..2a95d673 100644 --- a/Auth0Tests/Responses.swift +++ b/Auth0Tests/Responses.swift @@ -39,7 +39,8 @@ let UpdatedAtTimestamp = 1440004681.000 let CreatedAt = "2015-08-19T17:18:00.000Z" let CreatedAtUnix = "1440004680" let CreatedAtTimestamp = 1440004680.000 -let Sub = "auth0|\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))" +let Sub = "auth0|123456789" +let Kid = "key123" let LocaleUS = "en-US" let ZoneEST = "US/Eastern" let OTP = "123456" @@ -50,7 +51,6 @@ let RecoveryCode = "162534" let MFAToken = UUID().uuidString.replacingOccurrences(of: "-", with: "") let AuthenticatorId = UUID().uuidString.replacingOccurrences(of: "-", with: "") let ChallengeTypes = ["oob", "otp"] -let JWKKid = "key123" func authResponse(accessToken: String, idToken: String? = nil, refreshToken: String? = nil, expiresIn: Double? = nil) -> HTTPStubsResponse { var json = [ @@ -129,7 +129,7 @@ func managementErrorResponse(error: String, description: String, code: String, s return HTTPStubsResponse(jsonObject: ["code": code, "description": description, "statusCode": statusCode, "error": error], statusCode: Int32(statusCode), headers: ["Content-Type": "application/json"]) } -func jwksResponse(kid: String? = JWKKid) -> HTTPStubsResponse { +func jwksResponse(kid: String? = Kid) -> HTTPStubsResponse { var jwks: [String: Any] = ["keys": [["alg": "RS256", "kty": "RSA", "use": "sig", diff --git a/Auth0Tests/UserInfoSpec.swift b/Auth0Tests/UserInfoSpec.swift index 3f5c32e1..a998de78 100644 --- a/Auth0Tests/UserInfoSpec.swift +++ b/Auth0Tests/UserInfoSpec.swift @@ -23,9 +23,12 @@ import Foundation import Quick import Nimble +import JWTDecode @testable import Auth0 +fileprivate let BasicProfileJWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHwxMjM0NTY3ODkiLCJuYW1lIjoic3VwcG9ydCIsIm5pY2tuYW1lIjoic3VwIiwicGljdHVyZSI6Imh0dHBzOi8vYXV0aDAuY29tL3BpY3R1cmUiLCJ1cGRhdGVkX2F0IjoiMTQ0MDAwNDY4MSJ9.TppFbhhG2or0Ygtig_7wvMWj5pj1nibZQKlhp6YA0NnEmAU5oj9KxkL9BGCAjIUQcImO3Suiur27qNRDvTY7yG61kUfVFYmcdCcYZ3tuS2glA2Ofwjv-gkgkORFaggqwT4jaZ19MViHtW71AjH-l8Q9HbbCfD3pCI-M-95oSs7sPssXw3vOMbC_iMm-0TPzwSs32rc2Rmpni3T-rjthb7ZjYxpm2RUPvlpUMev0nb_E3QbLG-ct8jWwvDAjZbTgCYBkw0pmp57T4VBQ8acTQGvOi1lryrJ6kK9O9a_h9Yxf1t4HhBhfMW6p7fXNLVMYo5su3NFqW1KMVgUW7jNzKwA" + class UserInfoSpec: QuickSpec { override func spec() { @@ -110,6 +113,17 @@ class UserInfoSpec: QuickSpec { expect(userInfo?.locale?.identifier) == Locale(identifier: LocaleUS).identifier expect(userInfo?.zoneinfo?.identifier) == TimeZone(identifier: ZoneEST)!.identifier } + + it("should build from jwt body") { + let jwt = try! decode(jwt: BasicProfileJWT) + let userInfo = UserInfo(json: jwt.body) + expect(userInfo?.sub) == Sub + expect(userInfo?.name) == Support + expect(userInfo?.nickname) == Nickname + expect(userInfo?.picture) == PictureURL + expect(userInfo?.updatedAt?.timeIntervalSince1970) == UpdatedAtTimestamp + expect(userInfo?.customClaims).to(beEmpty()) + } }