Skip to content

Commit

Permalink
Merge pull request #100 from niscy-eudiw/feature/dpop-nonce
Browse files Browse the repository at this point in the history
Feature/dpop nonce
  • Loading branch information
dtsiflit authored Dec 16, 2024
2 parents 75baa73 + abf8430 commit 276778d
Show file tree
Hide file tree
Showing 25 changed files with 760 additions and 161 deletions.
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

0 comments on commit 276778d

Please sign in to comment.