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

Support id_token response type #62

Merged
merged 8 commits into from
Dec 2, 2016
Merged

Support id_token response type #62

merged 8 commits into from
Dec 2, 2016

Conversation

cocojoe
Copy link
Member

@cocojoe cocojoe commented Nov 29, 2016

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

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 {
Copy link
Member

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 + " "}
Copy link
Member

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()

Copy link
Member

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()

Copy link
Member Author

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.

Copy link
Member

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: " ")

Copy link
Member Author

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) {
Copy link
Member

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",
Copy link
Member

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 {
Copy link
Member

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) {
Copy link
Member

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 {
Copy link
Member

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 {
Copy link
Member

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
Reduced and merged response errors into webauth errors
Separate idtoken validation code

func credentials(from values: [String : String], callback: @escaping (Result<Credentials>) -> ()) {
// id token reponse expectations and JWT validation, nonce validation
if response.contains(.id_token) {
Copy link
Member

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

Copy link
Member Author

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) {
Copy link
Member

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 {
Copy link
Member

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

Copy link
Member Author

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 }
Copy link
Member

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?

Copy link
Member Author

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) {
Copy link
Member

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?
Copy link
Member

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?

Copy link
Member

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

Copy link
Member Author

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth2 -> WebAuth

Copy link
Member Author

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
Copy link
Member

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
Copy link
Member

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

Copy link
Member Author

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") {
Copy link
Member

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 {
Copy link
Member

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 {...}

Copy link
Member

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) {
Copy link
Member

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]) {
Copy link
Member

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 {
Copy link
Member

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

Copy link
Member

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)

Copy link
Member Author

@cocojoe cocojoe Dec 1, 2016

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?

Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalidIdTokenNonce

@hzalaz hzalaz changed the title Support id_token Support id_token response type Dec 2, 2016
@hzalaz hzalaz added this to the 1.1.0 milestone Dec 2, 2016
@hzalaz hzalaz merged commit aa0e751 into master Dec 2, 2016
@hzalaz hzalaz deleted the response_type_id_token branch January 29, 2017 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants