-
Notifications
You must be signed in to change notification settings - Fork 218
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
Support id_token response type #62
Conversation
Chaining method for response types Auto set implicit grant Credentials support for id_token only Chaining method for nonce Nonce required for id_token response type Tests added
|
||
import Foundation | ||
|
||
public enum ResponseType: String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OptionType is a best choice for this
if !responseType.contains(.code) { | ||
_ = self.usingImplicitGrant() | ||
} | ||
let response = responseType.reduce(String()) { $0 + $1.rawValue + " "} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to use literals whenever possible not String()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also you could use
responseType.joined(separator: " ")
and avoid the dropLast()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not as easy as that due to it being an enum and requiring rawValue, perhaps with the suggestion of using OptionSet
it will flow more easily. I'll look into it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
responseTypes.map { $0.rawValue }.joined(separator: " ")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -88,6 +102,11 @@ class SafariWebAuth: WebAuth { | |||
else { | |||
return callback(Result.failure(error: WebAuthError.noBundleIdentifierFound)) | |||
} | |||
if let response = self.parameters["response_type"], response.contains(ResponseType.id_token.rawValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
response_type
is mandatory and we should combine the if and guard in a single guard, it serves no purpose having them separated
@@ -63,6 +65,11 @@ public enum WebAuthError: CustomNSError { | |||
NSLocalizedDescriptionKey: message, | |||
WebAuthError.infoKey: self | |||
] | |||
case .noNonceProvided: | |||
return [ | |||
NSLocalizedDescriptionKey: "You must provide a nonce value for the id_token response type", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nonce value must be supplied when response_type includes id_token in order to prevent replay attacks
public let idToken: String? | ||
public let refreshToken: String? | ||
public var hasToken: Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't see the why we need this method, the hasToken
can only be known to the one who starts the auth or if we pass along the response_type
and validate. Thing we should do before yielding the credentials to the caller
|
||
required public init(accessToken: String, tokenType: String, idToken: String? = nil, refreshToken: String? = nil) { | ||
required public init(accessToken: String? = nil, tokenType: String? = nil, idToken: String? = nil, refreshToken: String? = nil) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sort of a breaking change so I'd need to figure out a way to communicate this, not a big one but still a breaking one
@@ -33,7 +33,7 @@ struct ImplicitGrant: OAuth2Grant { | |||
let defaults: [String : String] = ["response_type": "token"] | |||
|
|||
func credentials(from values: [String : String], callback: @escaping (Result<Credentials>) -> ()) { | |||
guard let credentials = Credentials(json: values as [String : Any]) else { | |||
guard let credentials = Credentials(json: values as [String : Any]), credentials.hasToken else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to validate that was requested was returned otherwise we need to fail.
@@ -76,6 +76,20 @@ class SafariWebAuth: WebAuth { | |||
return self | |||
} | |||
|
|||
func responseType(_ responseType: [ResponseType]) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to enforce that a response_type is picked here and by default should include code
Updated Carthage libs in line with Lock v2 JWTDecode added for nonce check in id_token ResponseType Enum changed to OptionSet Response Error enum added
Shuffled code around to be inline with builder expectations
53153cf
to
5ea5a35
Compare
|
||
func credentials(from values: [String : String], callback: @escaping (Result<Credentials>) -> ()) { | ||
// id token reponse expectations and JWT validation, nonce validation | ||
if response.contains(.id_token) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the if-guard? just use guard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple responseType are processed in this func
, guard
would exit if the responseType was not .id_token
, even .id_token
would then fail in the next proposed guard as it wouldn't contain .token
This applies to all my usage of if
then guard
.
} | ||
|
||
// token response expectations | ||
if response.contains(.token) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
@@ -97,3 +124,16 @@ struct PKCE: OAuth2Grant { | |||
return items | |||
} | |||
} | |||
|
|||
private func validate(token: String, nonce: String) -> Bool { | |||
do { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guard let jwt = try? decode(jwt: token), let nonce = jwt.claim(name: "nonce") else { return false }
return nonce.string == nonce
If we are not handling the error is useless to catch it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jwt.claim is non optional, agree it can be streamlined.
func usingImplicitGrant() -> Self { | ||
self.usePKCE = false | ||
func response(_ response: [ResponseOptions]) -> Self { | ||
if !response.contains(.code) { self.usePKCE = false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need the if?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably retire self.userPKCE and replace with a check to see if response contains .code
func start(_ callback: @escaping (Result<Credentials>) -> ()) { | ||
guard | ||
let redirectURL = self.redirectURL | ||
, !redirectURL.absoluteString.hasPrefix(SafariWebAuth.NoBundleIdentifier) | ||
else { | ||
return callback(Result.failure(error: WebAuthError.noBundleIdentifierFound)) | ||
} | ||
if self.response.contains(.id_token) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if-guard again, it's better to have just guard
|
||
public struct ResponseOptions: OptionSet { | ||
public let rawValue: Int | ||
public let label: String? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is part of the hack to use label right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use an enum? it may be better, we might need to provide a computed property name but it could be less hackish
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do this anyway, originally I had this but removed it for some reason. However in retrospect I think it's fine.
var label: String? {
switch self.rawValue {
case ResponseOptions.token.rawValue:
return "token"
case ResponseOptions.id_token.rawValue:
return "id_token"
case ResponseOptions.code.rawValue:
return "code"
default:
return nil
}
}
/// Setup the response types to be used for authentcation | ||
/// | ||
/// - Parameter response: Array of ResponseOptions | ||
/// - Returns: the same OAuth2 instance to allow method chaining |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OAuth2
-> WebAuth
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All existing documentation in this file references 'OAuth2', I'll rename them all then.
/// | ||
/// - Parameter response: Array of ResponseOptions | ||
/// - Returns: the same OAuth2 instance to allow method chaining | ||
func response(_ response: [ResponseOptions]) -> Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's stick with responseType
@@ -29,12 +29,17 @@ import Foundation | |||
- CannotDismissWebAuthController: When trying to dismiss WebAuth controller, no presenter controller could be found. | |||
- UserCancelled: User cancelled the web-based authentication, e.g. tapped the "Done" button in SFSafariViewController | |||
- PKCENotAllowed: PKCE for the supplied Auth0 ClientId was not allowed. You need to set the `Token Endpoint Authentication Method` to `None` in your Auth0 Dashboard | |||
- noNonceProvided: A nonce value must be provided to use the response option of id_token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing the last errors here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were added.
let credentials = Credentials(json: ["access_token": AccessToken]) | ||
expect(credentials).to(beNil()) | ||
} | ||
// it("should return nil when missing access_token") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about these?
ResponseOptions computed label Retired usePKCE, use ResponseType Updated Tests Updated Docsheaders
|
||
// token response expectations | ||
if responseType.contains(.token) { | ||
guard values["access_token"] != nil && values["token_type"] != nil else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guard !responseType.contains(.token) || values["access_token"] != nil else {...}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to account for tokenType
|
||
func credentials(from values: [String : String], callback: @escaping (Result<Credentials>) -> ()) { | ||
// id token reponse expectations and JWT validation, nonce validation | ||
if responseType.contains(.id_token) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move all logic of nonce validation to he validate method and then you can join guard and if into a guard.
public let idToken: String? | ||
public let refreshToken: String? | ||
|
||
required public init(accessToken: String, tokenType: String, idToken: String? = nil, refreshToken: String? = nil) { | ||
required public init(accessToken: String? = nil, tokenType: String? = nil, idToken: String? = nil, refreshToken: String? = nil) { | ||
self.accessToken = accessToken | ||
self.tokenType = tokenType | ||
self.idToken = idToken | ||
self.refreshToken = refreshToken | ||
} | ||
|
||
convenience required public init?(json: [String: Any]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are no longer returning nil so the init is not required to be a failable one
|
||
import Foundation | ||
|
||
public struct ResponseOptions: OptionSet { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make it an enum so we can avoid the dev adding strange values as response types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also ResponseType
might be more suitable as a name (plus missing docs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So after originally being an enum and saying to change it to OptionSet, you want to go back to enum again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot that enum won't be able to conform OptionSet
protocol
} | ||
|
||
public static let token = ResponseOptions(rawValue: 1 << 0) | ||
public static let id_token = ResponseOptions(rawValue: 1 << 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep with camelCase
case noNonceProvided | ||
case missingResponseParam(String) | ||
case idTokenValidationFailed | ||
case tokenValidationFailed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missingAccessToken
*/ | ||
public enum WebAuthError: CustomNSError { | ||
case noBundleIdentifierFound | ||
case cannotDismissWebAuthController | ||
case userCancelled | ||
case pkceNotAllowed(String) | ||
case noNonceProvided | ||
case missingResponseParam(String) | ||
case idTokenValidationFailed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
invalidIdTokenNonce
Chaining method for response types
Auto set implicit grant
Credentials support for id_token only
Chaining method for nonce
Nonce required for id_token response type
Tests added