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

Added support for custom issuer for ID Token validation [SDK-1909] #411

Merged
merged 10 commits into from
Aug 21, 2020
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
11 changes: 10 additions & 1 deletion Auth0/BaseWebAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ class BaseWebAuth: WebAuthenticatable {

private let platform: String
private(set) var parameters: [String: String] = [:]
private(set) var issuer: String
private(set) var leeway: Int = 60 * 1000 // Default leeway is 60 seconds
private var responseType: [ResponseType] = [.code]
private var nonce: String?
private var leeway: Int = 60 * 1000 // Default leeway is 60 seconds
private var maxAge: Int?

lazy var redirectURL: URL? = {
Expand All @@ -57,6 +58,7 @@ class BaseWebAuth: WebAuthenticatable {
self.url = url
self.storage = storage
self.telemetry = telemetry
self.issuer = "\(url.absoluteString)/"
}

func useUniversalLink() -> Self {
Expand Down Expand Up @@ -113,6 +115,11 @@ class BaseWebAuth: WebAuthenticatable {
return self
}

func issuer(_ issuer: String) -> Self {
self.issuer = issuer
return self
}

func leeway(_ leeway: Int) -> Self {
self.leeway = leeway
return self
Expand Down Expand Up @@ -246,12 +253,14 @@ class BaseWebAuth: WebAuthenticatable {
return PKCE(authentication: authentication,
redirectURL: redirectURL,
responseType: self.responseType,
issuer: self.issuer,
leeway: self.leeway,
maxAge: self.maxAge,
nonce: self.nonce)
}
return ImplicitGrant(authentication: authentication,
responseType: self.responseType,
issuer: self.issuer,
leeway: self.leeway,
maxAge: self.maxAge,
nonce: self.nonce)
Expand Down
4 changes: 2 additions & 2 deletions Auth0/IDTokenValidatorContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ struct IDTokenValidatorContext: IDTokenSignatureValidatorContext, IDTokenClaimsV
self.nonce = nonce
}

init(authentication: Authentication, leeway: Int, maxAge: Int?, nonce: String?) {
self.init(issuer: "\(authentication.url.absoluteString)/",
init(authentication: Authentication, issuer: String, leeway: Int, maxAge: Int?, nonce: String?) {
self.init(issuer: issuer,
audience: authentication.clientId,
jwksRequest: authentication.jwks(),
leeway: leeway,
Expand Down
11 changes: 6 additions & 5 deletions Auth0/JWK+RSA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ extension JWK {
}

private func encodeRSAPublicKey(modulus: [UInt8], exponent: [UInt8]) -> Data {
let encodedModulus = modulus.a0_derEncode(as: 2) // Integer
var prefixedModulus: [UInt8] = [0x00] // To indicate that the number is not negative
prefixedModulus.append(contentsOf: modulus)
let encodedModulus = prefixedModulus.a0_derEncode(as: 2) // Integer
let encodedExponent = exponent.a0_derEncode(as: 2) // Integer
let encodedSequence = (encodedModulus + encodedExponent).a0_derEncode(as: 48) // Sequence
return Data(encodedSequence)
Expand All @@ -50,11 +52,10 @@ extension JWK {
@available(iOS 10.0, macOS 10.12, *)
private func generateRSAPublicKey(from derEncodedData: Data) -> SecKey? {
let sizeInBits = derEncodedData.count * MemoryLayout<UInt8>.size
let attributes: [CFString: Any] = [kSecClass: kSecClassKey,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
let attributes: [CFString: Any] = [kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPublic,
kSecAttrAccessible: kSecAttrAccessibleAlways,
kSecAttrKeySizeInBits: NSNumber(value: sizeInBits)]
kSecAttrKeySizeInBits: NSNumber(value: sizeInBits),
kSecAttrIsPermanent: false]
return SecKeyCreateWithData(derEncodedData as CFData, attributes as CFDictionary, nil)
}
}
26 changes: 19 additions & 7 deletions Auth0/OAuth2Grant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ struct ImplicitGrant: OAuth2Grant {
let authentication: Authentication
let defaults: [String: String]
let responseType: [ResponseType]
let issuer: String
let leeway: Int
let maxAge: Int?

init(authentication: Authentication,
responseType: [ResponseType] = [.token],
issuer: String,
leeway: Int,
maxAge: Int? = nil,
nonce: String? = nil) {
self.authentication = authentication
self.responseType = responseType
self.issuer = issuer
self.leeway = leeway
self.maxAge = maxAge
if let nonce = nonce {
Expand All @@ -56,10 +59,11 @@ struct ImplicitGrant: OAuth2Grant {
func credentials(from values: [String: String], callback: @escaping (Result<Credentials>) -> Void) {
let responseType = self.responseType
let validatorContext = IDTokenValidatorContext(authentication: authentication,
issuer: issuer,
leeway: leeway,
maxAge: maxAge,
nonce: self.defaults["nonce"])
validate(idToken: values["id_token"], for: responseType, with: validatorContext) { error in
validateFrontChannelIDToken(idToken: values["id_token"], for: responseType, with: validatorContext) { error in
if let error = error { return callback(.failure(error: error)) }
guard !responseType.contains(.token) || values["access_token"] != nil else {
return callback(.failure(error: WebAuthError.missingAccessToken))
Expand All @@ -81,13 +85,15 @@ struct PKCE: OAuth2Grant {
let defaults: [String: String]
let verifier: String
let responseType: [ResponseType]
let issuer: String
let leeway: Int
let maxAge: Int?

init(authentication: Authentication,
redirectURL: URL,
generator: A0SHA256ChallengeGenerator = A0SHA256ChallengeGenerator(),
responseType: [ResponseType] = [.code],
issuer: String,
leeway: Int,
maxAge: Int? = nil,
nonce: String? = nil) {
Expand All @@ -97,6 +103,7 @@ struct PKCE: OAuth2Grant {
challenge: generator.challenge,
method: generator.method,
responseType: responseType,
issuer: issuer,
leeway: leeway,
maxAge: maxAge,
nonce: nonce)
Expand All @@ -108,13 +115,15 @@ struct PKCE: OAuth2Grant {
challenge: String,
method: String,
responseType: [ResponseType],
issuer: String,
leeway: Int,
maxAge: Int? = nil,
nonce: String? = nil) {
self.authentication = authentication
self.redirectURL = redirectURL
self.verifier = verifier
self.responseType = responseType
self.issuer = issuer
self.leeway = leeway
self.maxAge = maxAge
var newDefaults: [String: String] = [
Expand All @@ -127,6 +136,7 @@ struct PKCE: OAuth2Grant {
self.defaults = newDefaults
}

// swiftlint:disable function_body_length
func credentials(from values: [String: String], callback: @escaping (Result<Credentials>) -> Void) {
guard let code = values["code"] else {
let string = "No code found in parameters \(values)"
Expand All @@ -135,6 +145,7 @@ struct PKCE: OAuth2Grant {
let idToken = values["id_token"]
let responseType = self.responseType
let authentication = self.authentication
let issuer = self.issuer
let leeway = self.leeway
let nonce = self.defaults["nonce"]
let maxAge = self.maxAge
Expand All @@ -143,10 +154,11 @@ struct PKCE: OAuth2Grant {
let clientId = authentication.clientId
let isFrontChannelIdTokenExpected = responseType.contains(.idToken)
let validatorContext = IDTokenValidatorContext(authentication: authentication,
issuer: issuer,
leeway: leeway,
maxAge: maxAge,
nonce: nonce)
validate(idToken: idToken, for: responseType, with: validatorContext) { error in
validateFrontChannelIDToken(idToken: idToken, for: responseType, with: validatorContext) { error in
if let error = error { return callback(.failure(error: error)) }
authentication
.tokenExchange(withCode: code, codeVerifier: verifier, redirectURI: redirectUrlString)
Expand All @@ -159,7 +171,7 @@ struct PKCE: OAuth2Grant {
case .failure(let error): return callback(.failure(error: error))
case .success(let credentials):
guard isFrontChannelIdTokenExpected else {
return validate(idToken: credentials.idToken, for: responseType, with: validatorContext) { error in
return validate(idToken: credentials.idToken, with: validatorContext) { error in
if let error = error { return callback(.failure(error: error)) }
callback(result)
}
Expand All @@ -185,10 +197,10 @@ struct PKCE: OAuth2Grant {
}

// This method will skip the validation if the response type does not contain "id_token"
private func validate(idToken: String?,
for responseType: [ResponseType],
with context: IDTokenValidatorContext,
callback: @escaping (LocalizedError?) -> Void) {
private func validateFrontChannelIDToken(idToken: String?,
for responseType: [ResponseType],
with context: IDTokenValidatorContext,
callback: @escaping (LocalizedError?) -> Void) {
guard responseType.contains(.idToken) else { return callback(nil) }
validate(idToken: idToken, with: context) { error in
if let error = error { return callback(error) }
Expand Down
7 changes: 7 additions & 0 deletions Auth0/WebAuthenticatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ public protocol WebAuthenticatable: Trackable, Loggable {
/// - Returns: the same WebAuth instance to allow method chaining
func audience(_ audience: String) -> Self

/// Specify a custom issuer for ID Token validation.
/// This value will be used instead of the Auth0 domain.
///
/// - Parameter issuer: custom issuer value like: `https://example.com/`
/// - Returns: the same WebAuth instance to allow method chaining
func issuer(_ issuer: String) -> Self
Widcket marked this conversation as resolved.
Show resolved Hide resolved

/// Add a leeway amount for ID Token validation.
/// This value represents the clock skew for the validation of date claims e.g. `exp`.
///
Expand Down
24 changes: 12 additions & 12 deletions Auth0/_ObjectiveWebAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ public class _ObjectiveOAuth2: NSObject {
Before enabling this flag you'll need to configure Universal Links
*/
@objc public var universalLink: Bool {
set {
Widcket marked this conversation as resolved.
Show resolved Hide resolved
self.webAuth.universalLink = newValue
}
get {
return self.webAuth.universalLink
}
set {
self.webAuth.universalLink = newValue
}
}

/**
Expand All @@ -60,12 +60,12 @@ public class _ObjectiveOAuth2: NSObject {
Has no effect on older versions of iOS.
*/
@objc public var ephemeralSession: Bool {
set {
self.webAuth.ephemeralSession = newValue
}
get {
return self.webAuth.ephemeralSession
}
set {
self.webAuth.ephemeralSession = newValue
}
}

/**
Expand All @@ -74,28 +74,28 @@ public class _ObjectiveOAuth2: NSObject {
By default no connection is specified, so the hosted login page will be displayed
*/
@objc public var connection: String? {
get {
return self.webAuth.parameters["connection"]
}
set {
if let value = newValue {
_ = self.webAuth.connection(value)
}
}
get {
return self.webAuth.parameters["connection"]
}
}

/**
Scopes that will be requested during auth
*/
@objc public var scope: String? {
get {
return self.webAuth.parameters["scope"]
}
set {
if let value = newValue {
_ = self.webAuth.scope(value)
}
}
get {
return self.webAuth.parameters["scope"]
}
}

/**
Expand Down
22 changes: 12 additions & 10 deletions Auth0Tests/BaseAuthTransactionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import OHHTTPStubs

@testable import Auth0

private let ClientId = "CLIENT_ID"
private let Domain = URL(string: "https://samples.auth0.com")!
private let Issuer = "\(Domain.absoluteString)/"
private let Leeway = 60 * 1000
private let RedirectURL = URL(string: "https://samples.auth0.com/callback")!

class BaseAuthTransactionSpec: QuickSpec {

private let ClientId = "CLIENT_ID"
private let Domain = URL(string: "https://samples.auth0.com")!
private let Leeway = 60 * 1000
private let RedirectURL = URL(string: "https://samples.auth0.com/callback")!

override func spec() {
var transaction: BaseAuthTransaction!
Expand All @@ -43,25 +44,26 @@ class BaseAuthTransactionSpec: QuickSpec {
redirectURL: RedirectURL,
generator: generator,
responseType: [.code],
issuer: Issuer,
leeway: Leeway,
nonce: nil)
let idToken = generateJWT().string
let idToken = generateJWT(iss: Issuer, aud: [ClientId]).string
let code = "123456"

beforeEach {
transaction = BaseAuthTransaction(redirectURL: self.RedirectURL,
transaction = BaseAuthTransaction(redirectURL: RedirectURL,
state: "state",
handler: handler,
logger: nil,
callback: callback)
result = nil
stub(condition: isToken(self.Domain.host!) && hasAtLeast(["code": code,
stub(condition: isToken(Domain.host!) && hasAtLeast(["code": code,
"code_verifier": generator.verifier,
"grant_type": "authorization_code",
"redirect_uri": self.RedirectURL.absoluteString])) {
"redirect_uri": RedirectURL.absoluteString])) {
_ in return authResponse(accessToken: "AT", idToken: idToken)
}
stub(condition: isJWKSPath(self.Domain.host!)) { _ in jwksResponse() }
stub(condition: isJWKSPath(Domain.host!)) { _ in jwksResponse() }
}

afterEach {
Expand Down
2 changes: 1 addition & 1 deletion Auth0Tests/ChallengeGeneratorSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ChallengeGeneratorSpec: QuickSpec {
let seed: [UInt8] = [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
132, 141, 121]
let verifier = Data(bytes: UnsafePointer<UInt8>(seed), count: seed.count * MemoryLayout<UInt8>.size)
let verifier = Data(bytes: seed, count: seed.count * MemoryLayout<UInt8>.size)

let generator = A0SHA256ChallengeGenerator(verifier: verifier)

Expand Down
8 changes: 7 additions & 1 deletion Auth0Tests/Generators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ func generateRSAJWK(from publicKey: SecKey = TestKeys.rsaPublic, keyId: String =
guard let (end, exponent) = extractData(from: exponentBytes) else { return nil }
guard abs(end.distance(to: modulusBytes)) == totalLength else { return nil }

let encodedModulus = modulus.a0_encodeBase64URLSafe()
var mutableModulus = modulus

if mutableModulus.first == 0 {
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
mutableModulus.removeFirst() // See https://tools.ietf.org/html/rfc7518#section-6.3.1.1
}

let encodedModulus = mutableModulus.a0_encodeBase64URLSafe()
let encodedExponent = exponent.a0_encodeBase64URLSafe()

return JWK(keyType: "RSA",
Expand Down
15 changes: 13 additions & 2 deletions Auth0Tests/JWKSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ class JWKSpec: QuickSpec {
if #available(iOS 10.0, macOS 10.12, *) {
context("successful generation") {
it("should generate a RSA public key") {
let publicKey = jwk.rsaPublicKey!
let keyAttributes = SecKeyCopyAttributes(publicKey) as! [String: Any]
let publicKey = JWK(keyType: "RSA",
keyId: "NUZFNkFDNUVDNzIxMjAyQTU5RUEzQ0UyMEQ2Mjc5OUZFREFCQ0E2MA",
usage: "sig",
algorithm: JWTAlgorithm.rs256.rawValue,
certUrl: nil,
certThumbprint: nil,
certChain: nil,
rsaModulus: "42xFiJGFLj6e8PgJ-zDQE_KhXNscWFHmJylilVhpD0KUoNKict4IUBvmLYrKMiFLggBS-ttadXeJn7XMnsu6Dz8OzE6r9ELxjZK9sljwx-KWn3ojX8XB8c4LB4NLCEzcwAmE-1zEymJSRg7GJ1g5CHQ_uPeZgxPpEKg5XbrVjZO0KmKE2vCIEVFJIxXNIIu-yC4zR0dPLLEN0lPDZLwwYVRF5y9F_WzDX8fr2nGPQQHQdebBHe_ystvlNc1RdZvyM7BjN9z0l3CXTyR18bLNhJdRDU39NvS7IzGmnqL3WLAwZGtJ6rMhYCPsj-Dla4tUJCy6Yc4V7Gr8zBGQWmLKlQ",
rsaExponent: "AQAB").rsaPublicKey

expect(publicKey).notTo(beNil())

let keyAttributes = SecKeyCopyAttributes(publicKey!) as! [String: Any]

expect(keyAttributes[String(kSecAttrKeyType)] as? String).to(equal(String(kSecAttrKeyTypeRSA)))
}
Expand Down
Loading