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

Add a way to pass custom headers directly to the request [SDK-2908] #562

Merged
merged 7 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions Auth0.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@
970BC36C25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
970BC36D25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
970BC36E25C27095007A7745 /* Challenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970BC36A25C27095007A7745 /* Challenge.swift */; };
D581CF772757D773007327D1 /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D581CF762757D773007327D1 /* RequestSpec.swift */; };
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
D581CF782757D773007327D1 /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D581CF762757D773007327D1 /* RequestSpec.swift */; };
D581CF792757D773007327D1 /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D581CF762757D773007327D1 /* RequestSpec.swift */; };
D5E9E317273ACCA5000CDB0A /* ChallengeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */; };
D5E9E318273ACCA5000CDB0A /* ChallengeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -545,6 +548,7 @@
5FE686A91D1894AA0075874C /* TelemetrySpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetrySpec.swift; sourceTree = "<group>"; };
5FF465BB1CE2AC4500F7ED8C /* Management.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Management.swift; path = Auth0/Management.swift; sourceTree = SOURCE_ROOT; };
970BC36A25C27095007A7745 /* Challenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Challenge.swift; sourceTree = "<group>"; };
D581CF762757D773007327D1 /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeGenerator.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -984,6 +988,7 @@
isa = PBXGroup;
children = (
5F1FBB981D8A4465006B0B85 /* ResponseSpec.swift */,
D581CF762757D773007327D1 /* RequestSpec.swift */,
);
name = Networking;
sourceTree = "<group>";
Expand Down Expand Up @@ -1646,6 +1651,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D581CF772757D773007327D1 /* RequestSpec.swift in Sources */,
5F1FBB9C1D8A44C1006B0B85 /* ResponseSpec.swift in Sources */,
5C4F553123C9123000C89615 /* CryptoExtensions.swift in Sources */,
5FE686AA1D1894AA0075874C /* TelemetrySpec.swift in Sources */,
Expand Down Expand Up @@ -1683,6 +1689,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D581CF782757D773007327D1 /* RequestSpec.swift in Sources */,
5FE686AB1D1894AA0075874C /* TelemetrySpec.swift in Sources */,
5FBBF03C1CC96AA70024D2AF /* Responses.swift in Sources */,
5CE775AA244FCF4E00D054A0 /* JWTAlgorithmSpec.swift in Sources */,
Expand Down Expand Up @@ -1792,6 +1799,7 @@
files = (
5F331B0F1D4BB80700AE4382 /* Responses.swift in Sources */,
5B6EE39620F8AEDB00264AC7 /* CredentialsManagerSpec.swift in Sources */,
D581CF792757D773007327D1 /* RequestSpec.swift in Sources */,
5F331B061D4BB7DA00AE4382 /* CredentialsSpec.swift in Sources */,
5C53A7EA2703A23300A7C0A3 /* UserInfoSpec.swift in Sources */,
5F331B0B1D4BB7F900AE4382 /* UserPatchAttributesSpec.swift in Sources */,
Expand Down
18 changes: 11 additions & 7 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public struct CredentialsManager {
/// If no refresh token is available the endpoint is not called, the credentials are cleared, and the callback is invoked without an error.
///
/// - Parameter callback: callback with an error if the refresh token could not be revoked
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
public func revoke(_ callback: @escaping (CredentialsManagerError?) -> Void) {
/// - headers: additional headers to add to a possible token revokation. The headers will be set via Request.headers.
public func revoke(_ callback: @escaping (CredentialsManagerError?) -> Void, headers: [String: String] = [:]) {
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
guard
let data = self.storage.getEntry(forKey: self.storeKey),
let credentials = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Credentials.self, from: data),
Expand All @@ -124,6 +125,7 @@ public struct CredentialsManager {

self.authentication
.revoke(refreshToken: refreshToken)
.headers(headers)
.start { result in
switch result {
case .failure(let error):
Expand Down Expand Up @@ -160,11 +162,12 @@ public struct CredentialsManager {
/// - scope: scopes to request for the new tokens. By default is nil which will ask for the same ones requested during original Auth.
/// - minTTL: minimum time in seconds the access token must remain valid to avoid being renewed.
/// - parameters: additional parameters to add to a possible token refresh. The parameters will be set via Request.payload.
/// - headers: additional headers to add to a possible token refresh. The headers will be set via Request.headers.
/// - callback: callback with the user's credentials or the cause of the error.
/// - Important: This method only works for a refresh token obtained after auth with OAuth 2.0 API Authorization.
/// - Note: [Auth0 Refresh Tokens Docs](https://auth0.com/docs/tokens/concepts/refresh-tokens)
#if WEB_AUTH_PLATFORM
public func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
public func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], headers: [String: String] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
guard self.hasValid(minTTL: minTTL) else { return callback(.failure(.noCredentials)) }
if let bioAuth = self.bioAuth {
guard bioAuth.available else {
Expand All @@ -176,16 +179,16 @@ public struct CredentialsManager {
guard $0 == nil else {
return callback(.failure(CredentialsManagerError(code: .biometricsFailed, cause: $0!)))
}
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, callback: callback)
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, headers: headers, callback: callback)
}
} else {
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, callback: callback)
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, headers: headers, callback: callback)
}
}
#else
public func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
public func credentials(withScope scope: String? = nil, minTTL: Int = 0, parameters: [String: Any] = [:], headers: [String: String] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
guard self.hasValid(minTTL: minTTL) else { return callback(.failure(.noCredentials)) }
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, callback: callback)
self.retrieveCredentials(withScope: scope, minTTL: minTTL, parameters: parameters, headers: headers, callback: callback)
}
#endif

Expand All @@ -196,7 +199,7 @@ public struct CredentialsManager {
return credentials
}

private func retrieveCredentials(withScope scope: String?, minTTL: Int, parameters: [String: Any] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
private func retrieveCredentials(withScope scope: String?, minTTL: Int, parameters: [String: Any] = [:], headers: [String: String] = [:], callback: @escaping (CredentialsManagerResult<Credentials>) -> Void) {
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
self.dispatchQueue.async {
self.dispatchGroup.enter()

Expand All @@ -218,6 +221,7 @@ public struct CredentialsManager {
self.authentication
.renew(withRefreshToken: refreshToken, scope: scope)
.parameters(parameters)
.headers(headers)
.start { result in
switch result {
case .success(let credentials):
Expand Down
11 changes: 11 additions & 0 deletions Auth0/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,15 @@ public struct Request<T, E: Auth0APIError>: Requestable {

return Request(session: self.session, url: self.url, method: self.method, handle: self.handle, payload: parameter, headers: self.headers, logger: self.logger, telemetry: self.telemetry)
}

/**
Modify the headers by creating a copy of self and adding the provided headers to `headers`.

- parameter extraHeaders: Additional headers for the request. The provided map will be added to `headers`.
*/
public func headers(_ extraHeaders: [String: String]) -> Self {
let headers = extraHeaders.merging(self.headers) {(current, _) in current}

return Request(session: self.session, url: self.url, method: self.method, handle: self.handle, payload: self.payload, headers: headers, logger: self.logger, telemetry: self.telemetry)
}
}
25 changes: 25 additions & 0 deletions Auth0Tests/CredentialsManagerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ class CredentialsManagerSpec: QuickSpec {
}
}
}

it("should include custom headers") {
stub(condition: hasHeader("foo", value: "bar")) { _ in return revokeTokenResponse() }.name = "revoke success"
waitUntil(timeout: Timeout) { done in
credentialsManager.revoke({ error in
expect(error).to(beNil())
expect(credentialsManager.hasValid()).to(beFalse())
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
done()
}, headers: ["foo": "bar"])
}
}
}

describe("multi instances of credentials manager") {
Expand Down Expand Up @@ -484,6 +495,20 @@ class CredentialsManagerSpec: QuickSpec {
}
}
}

it("should include custom headers") {
stub(condition: hasHeader("foo", value: "bar")) {
_ in return authResponse(accessToken: NewAccessToken, idToken: NewIdToken, refreshToken: NewRefreshToken, expiresIn: ExpiresIn)
}
credentials = Credentials(accessToken: AccessToken, tokenType: TokenType, idToken: IdToken, refreshToken: RefreshToken, expiresIn: Date(timeIntervalSinceNow: -ExpiresIn))
_ = credentialsManager.store(credentials: credentials)
waitUntil(timeout: Timeout) { done in
credentialsManager.credentials(headers: ["foo": "bar"]) { result in
expect(result).to(haveCredentials(NewAccessToken, NewIdToken, NewRefreshToken))
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
done()
}
}
}
}

context("forced renew") {
Expand Down
9 changes: 7 additions & 2 deletions Auth0Tests/Matchers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,17 @@ func isMultifactorChallenge(_ domain: String) -> HTTPStubsTestBlock {
return isMethodPOST() && isHost(domain) && isPath("/mfa/challenge")
}

func hasBearerToken(_ token: String) -> HTTPStubsTestBlock {

func hasHeader(_ name: String, value: String) -> HTTPStubsTestBlock {
return { request in
return request.value(forHTTPHeaderField: "Authorization") == "Bearer \(token)"
return request.value(forHTTPHeaderField: name) == value
}
}

func hasBearerToken(_ token: String) -> HTTPStubsTestBlock {
return hasHeader("Authorization", value: "Bearer \(token)")
}

func containItem(withName name: String, value: String? = nil) -> Predicate<[URLQueryItem]> {
return Predicate<[URLQueryItem]>.define("contain item with name <\(name)>") { expression, failureMessage -> PredicateResult in
guard let items = try expression.evaluate() else { return PredicateResult(status: .doesNotMatch, message: failureMessage) }
Expand Down
43 changes: 43 additions & 0 deletions Auth0Tests/RequestSpec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation
import Quick
import Nimble

@testable import Auth0

private let url = URL(string: "https://samples.auth0.com")!

class RequestSpec: QuickSpec {
override func spec() {

describe("create and update request") {

it("should create a request with headers") {
let request = Request(session: URLSession.shared, url: url, method: "GET", handle: plainJson, headers: ["foo": "bar"], logger: nil, telemetry: Telemetry())
expect(request.headers["foo"]) == "bar"
}

it("should create a new request with extra headers") {
let request = Request(session: URLSession.shared, url: url, method: "GET", handle: plainJson, logger: nil, telemetry: Telemetry()).headers(["foo": "bar"])
expect(request.headers["foo"]) == "bar"
}

it("should merge extra headers with existing headers") {
let request = Request(session: URLSession.shared, url: url, method: "GET", handle: plainJson, headers: ["foo": "bar"], logger: nil, telemetry: Telemetry()).headers(["baz": "qux"])
expect(request.headers["foo"]) == "bar"
expect(request.headers["baz"]) == "qux"
}

it("should overwrite existing headers with extra headers") {
let request = Request(session: URLSession.shared, url: url, method: "GET", handle: plainJson, headers: ["foo": "bar"], logger: nil, telemetry: Telemetry()).headers(["foo": "baz"])
expect(request.headers["foo"]) == "baz"
}

it("should create a new request and not mutate an existing request") {
let request = Request(session: URLSession.shared, url: url, method: "GET", handle: plainJson, headers: ["foo": "bar"], logger: nil, telemetry: Telemetry())
expect(request.headers(["foo": "baz"]).headers["foo"]) == "baz"
expect(request.headers["foo"]) == "bar"
}

}
}
}