Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user property to CredentialsManager [SDK-2633] #482

Merged
merged 3 commits into from
Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 25 additions & 5 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) }
Expand Down
30 changes: 30 additions & 0 deletions Auth0Tests/CredentialsManagerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 3 additions & 7 deletions Auth0Tests/Generators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ import JWTDecode

@testable import Auth0

// MARK: - Constants

fileprivate let defaultKid = "key123"

// MARK: - Keys

@available(iOS 10.0, macOS 10.12, *)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -186,7 +182,7 @@ private func extractData(from bytes: UnsafePointer<UInt8>) -> (UnsafePointer<UIn
}

@available(iOS 10.0, macOS 10.12, *)
func generateRSAJWK(from publicKey: SecKey = TestKeys.rsaPublic, keyId: String = defaultKid) -> JWK {
func generateRSAJWK(from publicKey: SecKey = TestKeys.rsaPublic, keyId: String = Kid) -> JWK {
let asn = { (bytes: UnsafePointer<UInt8>) -> JWK? in
guard bytes.pointee == 0x30 else { return nil } // Checks that this is a SEQUENCE triplet

Expand Down
2 changes: 1 addition & 1 deletion Auth0Tests/IDTokenSignatureValidatorSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
6 changes: 3 additions & 3 deletions Auth0Tests/Responses.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 = [
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions Auth0Tests/UserInfoSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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())
}

}

Expand Down