Skip to content

Commit

Permalink
Fixes #187. (#211)
Browse files Browse the repository at this point in the history
* Adds two new initializers for AccessToken: `init(tokenString:refreshToken:expirationDate:grantedScopes:)` and `init?(oauthDictionary:)`
* Makes `AccessTokenFactory` public
* Adds additional test coverage to assert more parameters from JSON deserialization, and test new getters.
* Adds comments to document `AccessToken`'s `NSCoding` behavior
* Removes the `Codable` protocol from `AccessToken` since it won't be used for JSON serialization / deserialization
  • Loading branch information
edjiang authored Nov 28, 2017
1 parent c7b1100 commit de02a0e
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 95 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ When migrating to 0.8, you may need to add `import UberCore` to files previously
* `LoginManager` now uses `SFAuthenticationSession`, `SFSafariViewController`, or external Safari for web-based OAuth flows.
* `Deeplinking` protocol simplified. Public properties from the previous protocol is now available under the `.url` property.
* `UberAuthenticating` protocol simplified.
* `AccessToken` adds two new initializers intended to make custom OAuth flows easier. Fixes [Issue #187](https://github.com/uber/rides-ios-sdk/issues/187)

### Moved to UberCore

Expand Down
91 changes: 61 additions & 30 deletions source/UberCore/Authentication/Tokens/AccessToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

/// Stores information about an access token used for authorizing requests.
@objc(UBSDKAccessToken) public class AccessToken: NSObject, NSCoding, Decodable {

/**
Stores information about an access token used for authorizing requests.
This class implements NSCoding, but its representation is an internal representation
not compatible with the OAuth representation. Use an initializer if you want to serialize this
via an OAuth representation.
*/
@objc(UBSDKAccessToken) public class AccessToken: NSObject, NSCoding {
/// String containing the bearer token.
@objc public private(set) var tokenString: String?
@objc public private(set) var tokenString: String

/// String containing the refresh token.
@objc public private(set) var refreshToken: String?
Expand All @@ -40,15 +45,63 @@
/**
Initializes an AccessToken with the provided tokenString
- parameter tokenString: The tokenString to use for this AccessToken
- returns: an initialized AccessToken object
- parameter tokenString: The access token string
*/
@objc public init(tokenString: String) {
self.tokenString = tokenString
super.init()
}

/**
Initializes an AccessToken with the provided parameters
- parameter tokenString: The access token string
- parameter refreshToken: String containing the refresh token.
- parameter expirationDate: The expiration date for this access token
- parameter grantedScopes: The scopes this token is valid for
*/
@objc public init(tokenString: String,
refreshToken: String?,
expirationDate: Date?,
grantedScopes: [UberScope]) {
self.tokenString = tokenString
self.refreshToken = refreshToken
self.expirationDate = expirationDate
self.grantedScopes = grantedScopes
super.init()
}

/**
Initializes an AccessToken using a dictionary with key/values matching
the OAuth access token response.
See https://tools.ietf.org/html/rfc6749#section-5.1 for more details.
The `token_type` parameter is not required for this initializer.
- parameter oauthDictionary: A dictionary with key/values matching
the OAuth access token response.
*/
@objc public init?(oauthDictionary: [String: Any]) {
guard let tokenString = oauthDictionary["access_token"] as? String else { return nil }
self.tokenString = tokenString
self.refreshToken = oauthDictionary["refresh_token"] as? String
if let expiresIn = oauthDictionary["expires_in"] as? Double {
self.expirationDate = Date(timeIntervalSinceNow: expiresIn)
} else if let expiresIn = oauthDictionary["expires_in"] as? String,
let expiresInDouble = Double(expiresIn) {
self.expirationDate = Date(timeIntervalSinceNow: expiresInDouble)
}
self.grantedScopes = (oauthDictionary["scope"] as? String)?.toUberScopesArray() ?? []
}

// MARK: NSCoding methods.

/**
Note for reference. It would be better if these NSCoding methods allowed for serialization/deserialization for JSON.
However, this is used for serializing to Keychain via NSKeyedArchiver, and would take work to maintain backwards compatibility
if this was changed. Also, the OAuth `expires_in` parameter is a relative seconds string, which can't be stored by itself.
*/

/**
Initializer to build an accessToken from the provided NSCoder. Allows for
serialization of an AccessToken
Expand All @@ -58,7 +111,6 @@
- returns: An initialized AccessToken, or nil if something went wrong
*/
@objc public required init?(coder decoder: NSCoder) {
super.init()
guard let token = decoder.decodeObject(forKey: "tokenString") as? String else {
return nil
}
Expand All @@ -68,6 +120,7 @@
if let scopesString = decoder.decodeObject(forKey: "grantedScopes") as? String {
grantedScopes = scopesString.toUberScopesArray()
}
super.init()
}

/**
Expand All @@ -81,26 +134,4 @@
coder.encode(self.expirationDate, forKey: "expirationDate")
coder.encode(self.grantedScopes.toUberScopeString(), forKey: "grantedScopes")
}

/**
Mapping function used by ObjectMapper. Builds an AccessToken using the provided
Map data
- parameter map: The Map to use for populatng this AccessToken.
*/
enum CodingKeys: String, CodingKey {
case tokenString = "access_token"
case refreshToken = "refresh_token"
case expirationDate = "expiration_date"
case scopesString = "scope"
}

public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
tokenString = try container.decode(String.self, forKey: .tokenString)
refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken)
expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate)
let scopesString = try container.decodeIfPresent(String.self, forKey: .scopesString)
grantedScopes = scopesString?.toUberScopesArray() ?? []
}
}
41 changes: 25 additions & 16 deletions source/UberCore/Authentication/Tokens/AccessTokenFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ import Foundation
/**
Factory class to build access tokens
*/
@objc(UBSDKAccessTokenFactory) class AccessTokenFactory: NSObject {
@objc(UBSDKAccessTokenFactory) public class AccessTokenFactory: NSObject {
/**
Builds an AccessToken from the provided redirect URL
- throws: RidesAuthenticationError
- throws: UberAuthenticationError
- parameter url: The URL to parse the token from
- returns: An initialized AccessToken, or nil if one couldn't be created
*/
static func createAccessToken(fromRedirectURL redirectURL: URL) throws -> AccessToken {
public static func createAccessToken(fromRedirectURL redirectURL: URL) throws -> AccessToken {
guard var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false) else {
throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse)
}
Expand All @@ -60,23 +60,32 @@ Factory class to build access tokens
}
queryDictionary[queryItem.name] = value
}
if let error = queryDictionary["error"] as? String {

return try createAccessToken(from: queryDictionary)
}

/**
Builds an AccessToken from the provided JSON data
- throws: UberAuthenticationError
- parameter jsonData: The JSON Data to parse the token from
- returns: An initialized AccessToken
*/
public static func createAccessToken(fromJSONData jsonData: Data) throws -> AccessToken {
guard let responseDictionary = (try? JSONSerialization.jsonObject(with: jsonData, options: [])) as? [String: Any] else {
throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse)
}
return try createAccessToken(from: responseDictionary)
}

private static func createAccessToken(from oauthResponseDictionary: [String: Any]) throws -> AccessToken {
if let error = oauthResponseDictionary["error"] as? String {
guard let error = UberAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: error) else {
throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidRequest)
}
throw error
} else {
if let expiresInString = queryDictionary["expires_in"] as? String {
let expiresInSeconds = TimeInterval(atof(expiresInString))
let expirationDateSeconds = Date().timeIntervalSince1970 + expiresInSeconds
queryDictionary["expiration_date"] = expirationDateSeconds
queryDictionary.removeValue(forKey: "expires_in")
}

if let json = try? JSONSerialization.data(withJSONObject: queryDictionary, options: []),
let token = try? JSONDecoder.uberDecoder.decode(AccessToken.self, from: json) {
return token
}
} else if let token = AccessToken(oauthDictionary: oauthResponseDictionary) {
return token
}
throw UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .invalidResponse)
}
Expand Down
43 changes: 14 additions & 29 deletions source/UberCoreTests/AccessTokenFactoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ class AccessTokenFactoryTests: XCTestCase {
private let allowedScopesString = "profile history"
private let errorString = "invalid_parameters"

private let maxExpirationDifference = 2.0

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
Expand All @@ -54,21 +52,12 @@ class AccessTokenFactoryTests: XCTestCase {
return
}
do {
let expectedExpirationInterval = Date().timeIntervalSince1970 + expirationTime

let token : AccessToken = try AccessTokenFactory.createAccessToken(fromRedirectURL: url)
XCTAssertNotNil(token)
XCTAssertEqual(token.tokenString, tokenString)
XCTAssertEqual(token.refreshToken, refreshTokenString)
XCTAssertEqual(token.grantedScopes.toUberScopeString(), allowedScopesString)

guard let expiration = token.expirationDate?.timeIntervalSince1970 else {
XCTAssert(false)
return
}

let timeDiff = abs(expiration - expectedExpirationInterval)
XCTAssertLessThanOrEqual(timeDiff, maxExpirationDifference)
UBSDKAssert(date: token.expirationDate!, approximatelyIn: expirationTime)

} catch _ as NSError {
XCTAssert(false)
Expand Down Expand Up @@ -166,21 +155,12 @@ class AccessTokenFactoryTests: XCTestCase {
return
}
do {
let expectedExpirationInterval = Date().timeIntervalSince1970 + expirationTime

let token : AccessToken = try AccessTokenFactory.createAccessToken(fromRedirectURL: url)
XCTAssertNotNil(token)
XCTAssertEqual(token.tokenString, tokenString)
XCTAssertEqual(token.refreshToken, refreshTokenString)
XCTAssertEqual(token.grantedScopes.toUberScopeString(), allowedScopesString)

guard let expiration = token.expirationDate?.timeIntervalSince1970 else {
XCTAssert(false)
return
}

let timeDiff = abs(expiration - expectedExpirationInterval)
XCTAssertLessThanOrEqual(timeDiff, maxExpirationDifference)
UBSDKAssert(date: token.expirationDate!, approximatelyIn: expirationTime)

} catch {
XCTAssert(false)
Expand Down Expand Up @@ -210,18 +190,23 @@ class AccessTokenFactoryTests: XCTestCase {
}

func testParseValidJsonStringToAccessToken() {
let tokenString = "tokenString1234"
let jsonString = "{\"access_token\": \"\(tokenString)\"}"
let accessToken = try? JSONDecoder.uberDecoder.decode(AccessToken.self, from: jsonString.data(using: .utf8)!)

XCTAssertNotNil(accessToken)
XCTAssertEqual(accessToken?.tokenString, tokenString)
let jsonString = "{\"access_token\": \"\(tokenString)\", \"refresh_token\": \"\(refreshTokenString)\", \"expires_in\": \"\(expirationTime)\", \"scope\": \"\(allowedScopesString)\"}"

guard let accessToken = try? AccessTokenFactory.createAccessToken(fromJSONData: jsonString.data(using: .utf8)!) else {
XCTFail()
return
}
XCTAssertEqual(accessToken.tokenString, tokenString)
XCTAssertEqual(accessToken.refreshToken, refreshTokenString)
UBSDKAssert(date: accessToken.expirationDate!, approximatelyIn: expirationTime)
XCTAssert(accessToken.grantedScopes.contains(UberScope.profile))
XCTAssert(accessToken.grantedScopes.contains(UberScope.history))
}

func testParseInvalidJsonStringToAccessToken() {
let tokenString = "tokenString1234"
let jsonString = "{\"access_token\": \"\(tokenString)\""
let accessToken = try? JSONDecoder.uberDecoder.decode(AccessToken.self, from: jsonString.data(using: .utf8)!)
let accessToken = try? AccessTokenFactory.createAccessToken(fromJSONData: jsonString.data(using: .utf8)!)

XCTAssertNil(accessToken)
}
Expand Down
60 changes: 43 additions & 17 deletions source/UberCoreTests/OAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class OAuthTests: XCTestCase {
var error: NSError?
let timeout: TimeInterval = 2
let tokenString = "accessToken1234"
let refreshTokenString = "refresh"
let expiresIn = 10030.23
let scope = "profile history"

private var redirectURI: URL!

override func setUp() {
Expand All @@ -47,12 +51,6 @@ class OAuthTests: XCTestCase {
super.tearDown()
}

func testBuildinigWithString() {
let tokenString = "accessTokenString"
let token = AccessToken(tokenString: tokenString)
XCTAssertEqual(token.tokenString, tokenString)
}

/**
Test saving and object in keychain and retrieving it.
*/
Expand Down Expand Up @@ -123,24 +121,52 @@ class OAuthTests: XCTestCase {
XCTAssert(queryItems.contains(URLQueryItem(name: "redirect_uri", value: redirectURI.absoluteString)))
}

func testInitializeAccessTokenFromString() {
let token = AccessToken(tokenString: tokenString)
XCTAssertEqual(token.tokenString, tokenString)
}

func testInitializeAccessTokenFromOAuthDictionary() {
guard let token = tokenFixture() else {
XCTFail()
return
}
XCTAssertEqual(token.tokenString, tokenString)
XCTAssertEqual(token.refreshToken, refreshTokenString)
UBSDKAssert(date: token.expirationDate!, approximatelyIn: expiresIn)
XCTAssert(token.grantedScopes.contains(UberScope.profile))
XCTAssert(token.grantedScopes.contains(UberScope.history))
}

func loginCompletion() -> ((_ accessToken: AccessToken?, _ error: NSError?) -> Void) {
return { token, error in
self.accessToken = token
self.error = error
self.testExpectation.fulfill()
}
}

// Mark: Helper

func tokenFixture(_ accessToken: String = "accessToken1234") -> AccessToken?
{
var jsonDictionary = [String: Any]()
jsonDictionary["access_token"] = accessToken
jsonDictionary["refresh_token"] = refreshTokenString
jsonDictionary["expires_in"] = expiresIn
jsonDictionary["scope"] = scope
return AccessToken(oauthDictionary: jsonDictionary)
}
}

// Mark: Helper

func tokenFixture(_ accessToken: String = "token") -> AccessToken?
{
var jsonDictionary = [String: String]()
jsonDictionary["access_token"] = accessToken
jsonDictionary["refresh_token"] = "refresh"
jsonDictionary["expires_in"] = "10030.23"
jsonDictionary["scope"] = "profile history"
let jsonData = try! JSONEncoder().encode(jsonDictionary)
return try? JSONDecoder.uberDecoder.decode(AccessToken.self, from: jsonData)
extension XCTestCase {
func UBSDKAssert(date: Date, approximatelyEqualTo otherDate: Date, _ message: String = "") {
let allowedDifference: TimeInterval = 2
let difference = abs(date.timeIntervalSince(otherDate))
XCTAssert(difference < allowedDifference, message)
}

func UBSDKAssert(date: Date, approximatelyIn seconds: TimeInterval, _ message: String = "") {
UBSDKAssert(date: date, approximatelyEqualTo: Date(timeIntervalSinceNow: seconds), message)
}
}
2 changes: 0 additions & 2 deletions source/UberCoreTests/RefreshEndpointTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ class RefreshEndpointTests: XCTestCase {

XCTAssertTrue(queryItems.contains(expectedClientID))
XCTAssertTrue(queryItems.contains(expectedRefreshToken))



waitForExpectations(timeout: timeout, handler: { error in
if let error = error {
Expand Down
2 changes: 1 addition & 1 deletion source/UberRides/RidesClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ import UberCore
var accessToken: AccessToken?
if let data = response.data,
response.error == nil {
accessToken = try? JSONDecoder.uberDecoder.decode(AccessToken.self, from: data)
accessToken = try? AccessTokenFactory.createAccessToken(fromJSONData: data)
}
completion(accessToken, response)
}
Expand Down

0 comments on commit de02a0e

Please sign in to comment.