Skip to content

Commit

Permalink
(Feature)|Add customParameters to OAuth2AuthorizationResponse (#104)
Browse files Browse the repository at this point in the history
### Summary
This change will allow consuming applications of third party APIs to react accordingly when the user does not approve all requested scope permissions.

### Implementation
- Added `scope` property to the `OAuth2AuthorizationResponse` object as an array of Strings.
- Hooked this property up in the `OAuth2AuthorizationRedirectHandler`. The query string parameter `scope` is converted into a String array split by commas. This value is optional as users may choose to accept all requested permissions or none.

### Test Plan
- Added test to `OAuth2AuthorizationCodeTokenGrantStrategyTests` to verify the scope is not altered.
- Tested in live OAuth2SafariAuthorizationStrategy using Strava API, accepting and denying requested scope permissions.
  • Loading branch information
anthony-lipscomb-dev authored Aug 30, 2018
1 parent c25fa79 commit 86141e8
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 2 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
#### Other
- None

## 0.12.0

#### Breaking
- None

#### Enhancements
- Add scope to OAuth2AuthorizationResponse

#### Bug Fixes
- None

#### Other
- None

## 0.11.0

Expand Down
8 changes: 8 additions & 0 deletions Conduit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,9 @@
95E26BB21F29A2FE00804900 /* XMLNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E26BB11F29A2FE00804900 /* XMLNodeTests.swift */; };
95E26BB31F29A2FE00804900 /* XMLNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E26BB11F29A2FE00804900 /* XMLNodeTests.swift */; };
95E26BB41F29A2FE00804900 /* XMLNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E26BB11F29A2FE00804900 /* XMLNodeTests.swift */; };
D2418216213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2418215213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift */; };
D2418217213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2418215213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift */; };
D2418218213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2418215213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -575,6 +578,7 @@
95E26BA71F29649600804900 /* XMLNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLNode.swift; sourceTree = "<group>"; };
95E26BB11F29A2FE00804900 /* XMLNodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLNodeTests.swift; sourceTree = "<group>"; };
95F04E761F7C3B21003E9700 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
D2418215213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2AuthorizationStrategyTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -660,6 +664,7 @@
1B279CBC1F19558100C0005E /* OAuth2RequestPipelineMiddlewareTests.swift */,
1B279CBD1F19558100C0005E /* OAuth2TokenGrantManagerTests.swift */,
1B279CBE1F19558100C0005E /* OAuth2TokenStorageTests.swift */,
D2418215213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift */,
);
path = Auth;
sourceTree = "<group>";
Expand Down Expand Up @@ -1776,6 +1781,7 @@
1BD555911F17DC81004B1172 /* JSONResponseDeserializerTests.swift in Sources */,
1BD555971F17DC81004B1172 /* QueryStringTests.swift in Sources */,
1BD555701F17DC81004B1172 /* NetworkReachabilityTests.swift in Sources */,
D2418216213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */,
1B279CBF1F19558100C0005E /* AuthTestUtilities.swift in Sources */,
1B279CD71F19558100C0005E /* OAuth2PasswordTokenGrantTests.swift in Sources */,
1B02EC911F60BE5200A41B34 /* ConduitLoggerTests.swift in Sources */,
Expand Down Expand Up @@ -1817,6 +1823,7 @@
1BD555921F17DC81004B1172 /* JSONResponseDeserializerTests.swift in Sources */,
1BD555981F17DC81004B1172 /* QueryStringTests.swift in Sources */,
1BD555711F17DC81004B1172 /* NetworkReachabilityTests.swift in Sources */,
D2418217213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */,
1B279CC01F19558100C0005E /* AuthTestUtilities.swift in Sources */,
1B279CD81F19558100C0005E /* OAuth2PasswordTokenGrantTests.swift in Sources */,
1B02EC921F60BE5200A41B34 /* ConduitLoggerTests.swift in Sources */,
Expand Down Expand Up @@ -1858,6 +1865,7 @@
1BD555931F17DC81004B1172 /* JSONResponseDeserializerTests.swift in Sources */,
1BD555991F17DC81004B1172 /* QueryStringTests.swift in Sources */,
1BD555721F17DC81004B1172 /* NetworkReachabilityTests.swift in Sources */,
D2418218213604A900D70220 /* OAuth2AuthorizationStrategyTests.swift in Sources */,
1B279CC11F19558100C0005E /* AuthTestUtilities.swift in Sources */,
1B279CD91F19558100C0005E /* OAuth2PasswordTokenGrantTests.swift in Sources */,
1B02EC931F60BE5200A41B34 /* ConduitLoggerTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ public struct OAuth2AuthorizationResponse {
/// callback. This is primarily used to prevent CSRF attacks.
public let state: String?

/// Any additional or custom parameters not explicitly stated in the OAuth2 Spec.
/// This could include accepted scope or granted permissions.
public let customParameters: [String: String]

/// Creates a new OAuth2AuthorizationResponse
/// - Parameters:
/// - code: The authorization code to be supplied to the authorization_code grant
/// - state: (Optional) An opaque value used by the client to maintain state between request and the
/// callback. This is primarily used to prevent CSRF attacks.
public init(code: String, state: String? = nil) {
public init(code: String, state: String? = nil, customParameters: [String: String] = [:]) {
self.code = code
self.state = state
self.customParameters = customParameters
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public class OAuth2AuthorizationRedirectHandler: NSObject {
}

let state = queryItemsDict["state"]
let response = OAuth2AuthorizationResponse(code: code, state: state)
let response = OAuth2AuthorizationResponse(code: code, state: state, customParameters: queryItemsDict)
activeHandler?(.value(response))
return shouldHandleURL
}
Expand Down
26 changes: 26 additions & 0 deletions Tests/ConduitTests/Auth/AuthTestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
@testable import Conduit

class AuthTestUtilities {

Expand All @@ -30,4 +31,29 @@ class AuthTestUtilities {
return params
}

static func makeSecureRandom(length: Int) -> String {
var buffer = [UInt8](repeating: 0, count: length)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
return base64URLEncode(Data(bytes: buffer))
}

private static func base64URLEncode(_ input: Data) -> String {
return input.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
.trimmingCharacters(in: .whitespaces)
}

}

class MockSafariAuthorizationStrategy: NSObject, OAuth2AuthorizationStrategy {
func authorize(request: OAuth2AuthorizationRequest, completion: @escaping (Result<OAuth2AuthorizationResponse>) -> Void) {
let state = request.state
let code = AuthTestUtilities.makeSecureRandom(length: 32)
var params = request.additionalParameters ?? [:]
params["scope"] = request.scope
let response = OAuth2AuthorizationResponse(code: code, state: state, customParameters: params)
completion(.value(response))
}
}
45 changes: 45 additions & 0 deletions Tests/ConduitTests/Auth/OAuth2AuthorizationStrategyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// OAuth2AuthorizationStrategyTests.swift
// Conduit
//
// Created by Anthony Lipscomb on 8/28/18.
// Copyright © 2018 MINDBODY. All rights reserved.
//

import XCTest
@testable import Conduit

class OAuth2AuthorizationStrategyTests: XCTestCase {

let redirectURI = "x-oauth2-myapp://authorize"
let customParameters: [String: String] = ["some_id": "123abc"]
let clientIdentifier: String = "Conduit"
let scope = "private_read,write"

private func makeStrategy() throws -> MockSafariAuthorizationStrategy {
return MockSafariAuthorizationStrategy()
}

func testAuthorize() throws {
var request = OAuth2AuthorizationRequest(clientIdentifier: clientIdentifier)
request.redirectURI = try URL(absoluteString: redirectURI)
request.scope = scope
request.state = AuthTestUtilities.makeSecureRandom(length: 32)
request.clientSecret = "shhh, it's a secret"
request.additionalParameters = customParameters

let expect = expectation(description: "Expect the query parameters to be returned in the header of the resposne")

var response: OAuth2AuthorizationResponse!
try makeStrategy().authorize(request: request) { authorizeResponse in
XCTAssertNil(authorizeResponse.error)
response = authorizeResponse.value
expect.fulfill()
}

wait(for: [expect], timeout: TimeInterval(5))
XCTAssertNotNil(response.code)
XCTAssert(response.customParameters.contains { $0 == "scope" && $1 == scope } == true)
XCTAssertEqual(response.state, request.state)
}
}

0 comments on commit 86141e8

Please sign in to comment.