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

Feature/dpop nonce #100

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
21 changes: 15 additions & 6 deletions Sources/DPoP/DPoPConstructor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ import JOSESwift
import CryptoKit

public protocol DPoPConstructorType {
func jwt(endpoint: URL, accessToken: String?) async throws -> String
func jwt(
endpoint: URL,
accessToken: String?,
nonce: Nonce?
) async throws -> String
}

public class DPoPConstructor: DPoPConstructorType {

static let type = "dpop+jwt"

private enum Methods: String {
case get = "GET"
case head = "HEAD"
Expand All @@ -46,13 +52,14 @@ public class DPoPConstructor: DPoPConstructorType {

public func jwt(
endpoint: URL,
accessToken: String?
accessToken: String?,
nonce: Nonce?
) async throws -> String {

let header = try JWSHeader(parameters: [
"typ": "dpop+jwt",
"alg": algorithm.name,
"jwk": jwk.toDictionary()
JWTClaimNames.type: Self.type,
JWTClaimNames.algorithm: algorithm.name,
JWTClaimNames.JWK: jwk.toDictionary()
])

var dictionary: [String: Any] = [
Expand All @@ -61,11 +68,13 @@ public class DPoPConstructor: DPoPConstructorType {
JWTClaimNames.htu: endpoint.absoluteString,
JWTClaimNames.jwtId: String.randomBase64URLString(length: 20)
]

nonce.map { dictionary[JWTClaimNames.nonce] = $0.value }

if let data = accessToken?.data(using: .utf8) {
let hashed = SHA256.hash(data: data)
let hash = Data(hashed).base64URLEncodedString()
dictionary["ath"] = hash
dictionary[JWTClaimNames.ath] = hash
}

let payload = Payload(try dictionary.toThrowingJSONData())
Expand Down
8 changes: 4 additions & 4 deletions Sources/Entities/Errors/GenericErrorResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public struct GenericErrorResponse: Codable {

public init(
error: String,
errorDescription: String?,
cNonce: String?,
cNonceExpiresInSeconds: Int?,
interval: Int?
errorDescription: String? = nil,
cNonce: String? = nil,
cNonceExpiresInSeconds: Int? = nil,
interval: Int? = nil
) {
self.error = error
self.errorDescription = errorDescription
Expand Down
3 changes: 3 additions & 0 deletions Sources/Entities/Errors/ValidationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum ValidationError: Error, LocalizedError {
case response(GenericErrorResponse)
case invalidBatchSize(Int)
case issuerBatchSizeLimitExceeded(Int)
case retryFailedAfterDpopNonce

public var errorDescription: String? {
switch self {
Expand All @@ -40,6 +41,8 @@ public enum ValidationError: Error, LocalizedError {
return "ValidationError:invalidBatchSize: \(size)"
case .issuerBatchSizeLimitExceeded(let size):
return "ValidationError:issuerBatchSizeLimitExceeded: \(size)"
case .retryFailedAfterDpopNonce:
return "retryFailedAfterDpopNonce"
}
}
}
32 changes: 22 additions & 10 deletions Sources/Entities/Issuance/AuthorizedRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ public enum AuthorizedRequest {
accessToken: IssuanceAccessToken,
refreshToken: IssuanceRefreshToken?,
credentialIdentifiers: AuthorizationDetailsIdentifiers?,
timeStamp: TimeInterval
timeStamp: TimeInterval,
dPopNonce: Nonce?
)
case proofRequired(
accessToken: IssuanceAccessToken,
refreshToken: IssuanceRefreshToken?,
cNonce: CNonce,
credentialIdentifiers: AuthorizationDetailsIdentifiers?,
timeStamp: TimeInterval
timeStamp: TimeInterval,
dPopNonce: Nonce?
)

public func isAccessTokenExpired(clock: TimeInterval) -> Bool {
Expand All @@ -69,16 +71,25 @@ public enum AuthorizedRequest {

public var timeStamp: TimeInterval? {
switch self {
case .noProofRequired(_, _, _, let timeStamp):
case .noProofRequired(_, _, _, let timeStamp, _):
return timeStamp
case .proofRequired(_, _, _, _, let timeStamp):
case .proofRequired(_, _, _, _, let timeStamp, _):
return timeStamp
}
}

public var dPopNonce: Nonce? {
switch self {
case .noProofRequired(_, _, _, _, let dPopNonce):
return dPopNonce
case .proofRequired(_, _, _, _, _, let dPopNonce):
return dPopNonce
}
}

public var noProofToken: IssuanceAccessToken? {
switch self {
case .noProofRequired(let accessToken, _, _, _):
case .noProofRequired(let accessToken, _, _, _, _):
return accessToken
case .proofRequired:
return nil
Expand All @@ -89,7 +100,7 @@ public enum AuthorizedRequest {
switch self {
case .noProofRequired:
return nil
case .proofRequired(let accessToken, _, _, _, _):
case .proofRequired(let accessToken, _, _, _, _, _):
return accessToken
}
}
Expand All @@ -98,23 +109,24 @@ public enum AuthorizedRequest {
public extension AuthorizedRequest {
var accessToken: IssuanceAccessToken? {
switch self {
case .noProofRequired(let accessToken, _, _, _):
case .noProofRequired(let accessToken, _, _, _, _):
return accessToken
case .proofRequired(let accessToken, _, _, _, _):
case .proofRequired(let accessToken, _, _, _, _, _):
return accessToken
}
}

func handleInvalidProof(cNonce: CNonce) throws -> AuthorizedRequest {
switch self {

case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers, let timeStamp):
case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers, let timeStamp, let dPopNonce):
return .proofRequired(
accessToken: accessToken,
refreshToken: refreshToken,
cNonce: cNonce,
credentialIdentifiers: credentialIdentifiers,
timeStamp: timeStamp
timeStamp: timeStamp,
dPopNonce: dPopNonce
)
default: throw ValidationError.error(reason: "Expected .noProofRequired authorisation request")
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/Entities/Issuance/UnauthorizedRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,22 @@ public struct ParRequested {
public let pkceVerifier: PKCEVerifier
public let state: String
public let configurationIds: [CredentialConfigurationIdentifier]
public let dpopNonce: Nonce?

public init(
credentials: [CredentialIdentifier],
getAuthorizationCodeURL: GetAuthorizationCodeURL,
pkceVerifier: PKCEVerifier,
state: String,
configurationIds: [CredentialConfigurationIdentifier]
configurationIds: [CredentialConfigurationIdentifier],
dpopNonce: Nonce? = nil
) {
self.credentials = credentials
self.getAuthorizationCodeURL = getAuthorizationCodeURL
self.pkceVerifier = pkceVerifier
self.state = state
self.configurationIds = configurationIds
self.dpopNonce = dpopNonce
}
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/Entities/IssuanceAccessToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@ public extension IssuanceAccessToken {

func dPoPOrBearerAuthorizationHeader(
dpopConstructor: DPoPConstructorType?,
dPopNonce: Nonce?,
endpoint: URL?
) async throws -> [String: String] {
if tokenType == TokenType.bearer {
return ["Authorization": "\(TokenType.bearer.rawValue) \(accessToken)"]
} else if let dpopConstructor, tokenType == TokenType.dpop, let endpoint {
let jwt = try await dpopConstructor.jwt(endpoint: endpoint, accessToken: accessToken)
let jwt = try await dpopConstructor.jwt(
endpoint: endpoint,
accessToken: accessToken,
nonce: dPopNonce
)
return [
"Authorization": "\(TokenType.dpop.rawValue) \(accessToken)",
TokenType.dpop.rawValue: jwt
Expand Down
4 changes: 4 additions & 0 deletions Sources/Entities/Types/JWTClaimNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public extension JWTClaimNames {
static let nonce = "nonce"
static let htm = "htm"
static let htu = "htu"
static let ath = "ath"
static let type = "typ"
static let algorithm = "alg"
static let JWK = "jwk"
}
8 changes: 8 additions & 0 deletions Sources/Entities/Types/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ public struct CNonce: Codable {
}
}

public struct Nonce {
public let value: String

public init(value: String) {
self.value = value
}
}

public struct Claim: Codable {
public let mandatory: Bool?
public let valueType: String?
Expand Down
38 changes: 38 additions & 0 deletions Sources/Extensions/HTTPURLResponse+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation

public extension HTTPURLResponse {

private func valueForHeader(_ header: String) -> String? {
let lowercasedHeader = header.lowercased()
for (key, value) in allHeaderFields {
if let keyString = key as? String, keyString.lowercased() == lowercasedHeader {
return value as? String
}
}
return nil
}

func containsDpopError() -> Bool {
guard statusCode == HTTPStatusCode.unauthorized,
let wwwAuth = valueForHeader("WWW-Authenticate") else {
return false
}
return wwwAuth.contains("DPoP") && wwwAuth.contains("error=\"use_dpop_nonce\"")
}
}

Loading
Loading