Skip to content

Commit

Permalink
Merge pull request #39 from niscy-eudiw/feature/dpop-support
Browse files Browse the repository at this point in the history
Feature - DPoP support
  • Loading branch information
dtsiflit authored Jun 5, 2024
2 parents 68e2538 + 6e8bd14 commit 831495d
Show file tree
Hide file tree
Showing 23 changed files with 650 additions and 107 deletions.
89 changes: 89 additions & 0 deletions Sources/DPoP/DPoPConstructor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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
import JOSESwift
import CryptoKit

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

public class DPoPConstructor: DPoPConstructorType {

private enum Methods: String {
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case connect = "CONNECT"
case options = "OPTIONS"
case trace = "TRACE"
}

public let algorithm: JWSAlgorithm
public let jwk: JWK
public let privateKey: SecKey

public init(algorithm: JWSAlgorithm, jwk: JWK, privateKey: SecKey) {
self.algorithm = algorithm
self.jwk = jwk
self.privateKey = privateKey
}

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

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

var dictionary: [String: Any] = [
JWTClaimNames.issuedAt: Int(Date().timeIntervalSince1970.rounded()),
JWTClaimNames.htm: Methods.post.rawValue,
JWTClaimNames.htu: endpoint.absoluteString,
JWTClaimNames.jwtId: String.randomBase64URLString(length: 20)
]

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

let payload = Payload(try dictionary.toThrowingJSONData())

guard let signatureAlgorithm = SignatureAlgorithm(rawValue: algorithm.name) else {
throw CredentialIssuanceError.cryptographicAlgorithmNotSupported
}

guard let signer = Signer(
signingAlgorithm: signatureAlgorithm,
key: privateKey
) else {
throw ValidationError.error(reason: "Unable to create JWS signer")
}

let jws = try JWS(
header: header,
payload: payload,
signer: signer
)

return jws.compactSerializedString
}
}
33 changes: 33 additions & 0 deletions Sources/Entities/AuthorizationToken.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 enum AuthorizationToken: Codable {

case bearer(accessToken: String)
case dpop(accessToken: String)

public init(accessToken: String, useDPoP: Bool) throws {
if accessToken.isEmpty {
throw ValidationError.error(reason: "AuthorizationToken access token cannot be empty")
}
if useDPoP {
self = .dpop(accessToken: accessToken)
} else {
self = .bearer(accessToken: accessToken)
}
}
}
6 changes: 0 additions & 6 deletions Sources/Entities/Encryption/BindingKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ public extension BindingKey {
throw CredentialIssuanceError.proofTypeNotSupported
}

/*
let bindings = spec.cryptographicBindingMethodsSupported.contains { $0 == .jwk }
guard bindings else {
throw CredentialIssuanceError.cryptographicBindingMethodNotSupported
}
*/
let aud = issuanceRequester.issuerMetadata.credentialIssuerIdentifier.url.absoluteString

let header = try JWSHeader(parameters: [
Expand Down
30 changes: 17 additions & 13 deletions Sources/Entities/Issuance/AuthorizedRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@ import Foundation

public enum AuthorizedRequest {
case noProofRequired(
token: IssuanceAccessToken,
accessToken: IssuanceAccessToken,
refreshToken: IssuanceRefreshToken?,
credentialIdentifiers: AuthorizationDetailsIdentifiers?
)
case proofRequired(
token: IssuanceAccessToken,
accessToken: IssuanceAccessToken,
refreshToken: IssuanceRefreshToken?,
cNonce: CNonce,
credentialIdentifiers: AuthorizationDetailsIdentifiers?
)

public var noProofToken: IssuanceAccessToken? {
switch self {
case .noProofRequired(let token, _):
return token
case .noProofRequired(let accessToken, _, _):
return accessToken
case .proofRequired:
return nil
}
Expand All @@ -39,28 +41,30 @@ public enum AuthorizedRequest {
switch self {
case .noProofRequired:
return nil
case .proofRequired(let token, _, _):
return token
case .proofRequired(let accessToken, _, _, _):
return accessToken
}
}
}

public extension AuthorizedRequest {
var accessToken: IssuanceAccessToken? {
switch self {
case .noProofRequired(let token, _):
return token
case .proofRequired(let token, _, _):
return token
case .noProofRequired(let accessToken, _, _):
return accessToken
case .proofRequired(let accessToken, _, _, _):
return accessToken
}
}

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

case .noProofRequired(let token, let credentialIdentifiers):
case .noProofRequired(let accessToken, let refreshToken, let credentialIdentifiers):
return .proofRequired(
token: token,
cNonce: cNonce,
accessToken: accessToken,
refreshToken: refreshToken,
cNonce: cNonce,
credentialIdentifiers: credentialIdentifiers
)
default: throw ValidationError.error(reason: "Expected .noProofRequired authorisation request")
Expand Down
42 changes: 41 additions & 1 deletion Sources/Entities/IssuanceAccessToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,59 @@
*/
import Foundation

public enum TokenType: String, Codable {
case bearer = "Bearer"
case dpop = "DPoP"

public init(value: String?) {
guard let value else {
self = .bearer
return
}

if value == TokenType.bearer.rawValue {
self = .bearer
} else if value == TokenType.dpop.rawValue {
self = .dpop
} else {
self = .bearer
}
}
}

public struct IssuanceAccessToken: Codable {
public let accessToken: String
public let tokenType: TokenType?

public init(accessToken: String) throws {
public init(
accessToken: String,
tokenType: TokenType?
) throws {
guard !accessToken.isEmpty else {
throw ValidationError.error(reason: "Access token cannot be empty")
}
self.accessToken = accessToken
self.tokenType = tokenType
}
}

public extension IssuanceAccessToken {
var authorizationHeader: [String: String] {
["Authorization": "BEARER \(accessToken)"]
}

func dPoPOrBearerAuthorizationHeader(
dpopConstructor: DPoPConstructorType?,
endpoint: URL?
) throws -> [String: String] {
if tokenType == TokenType.bearer {
return ["Authorization": "BEARER \(accessToken)"]
} else if let dpopConstructor, tokenType == TokenType.dpop, let endpoint {
return [
"Authorization": "DPoP \(accessToken)",
"DPoP": try dpopConstructor.jwt(endpoint: endpoint, accessToken: accessToken)
]
}
return ["Authorization": "BEARER \(accessToken)"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public extension MsoMdocClaims {
}

public enum CredentialIssuanceRequest {
case single(SingleCredential)
case single(SingleCredential, IssuanceResponseEncryptionSpec?)
case batch([SingleCredential])
}

Expand Down
25 changes: 25 additions & 0 deletions Sources/Entities/IssuanceRefreshToken.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 struct IssuanceRefreshToken: Codable {
public let refreshToken: String?

public init(refreshToken: String?) throws {
self.refreshToken = refreshToken
}
}

2 changes: 1 addition & 1 deletion Sources/Entities/Profiles/MsoMdocFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ public extension MsoMdocFormat {
claimSet: try claimSet?.validate(claims: self.claimList),
credentialIdentifier: credentialIdentifier
)
)
), responseEncryptionSpec
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Entities/Profiles/SdJwtVcFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ public extension SdJwtVcFormat {
),
credentialIdentifier: credentialIdentifier
)
)
), responseEncryptionSpec
)
}
}
Expand Down
21 changes: 20 additions & 1 deletion Sources/Entities/Response/AccessTokenRequestResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public typealias AuthorizationDetailsIdentifiers = [CredentialConfigurationIdent

public enum AccessTokenRequestResponse: Codable {
case success(
tokenType: String?,
accessToken: String,
refreshToken: String?,
expiresIn: Int,
scope: String?,
cNonce: String?,
Expand All @@ -33,7 +35,9 @@ public enum AccessTokenRequestResponse: Codable {
)

enum CodingKeys: String, CodingKey {
case tokenType = "token_type"
case accessToken = "access_token"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
case scope
case error
Expand All @@ -49,6 +53,8 @@ public enum AccessTokenRequestResponse: Codable {
if let accessToken = try? container.decode(String.self, forKey: .accessToken),
let expiresIn = try? container.decode(Int.self, forKey: .expiresIn) {

let tokenType = try? container.decode(String.self, forKey: .tokenType)
let refeshToken = try? container.decode(String.self, forKey: .refreshToken)
var authorizationDetails: AuthorizationDetailsIdentifiers = [:]

let json = try? container.decode(JSON.self, forKey: .authorizationDetails)
Expand All @@ -71,7 +77,9 @@ public enum AccessTokenRequestResponse: Codable {
}

self = .success(
tokenType: tokenType,
accessToken: accessToken,
refreshToken: refeshToken,
expiresIn: expiresIn,
scope: try? container.decode(String.self, forKey: .scope),
cNonce: try? container.decode(String.self, forKey: .cNonce),
Expand All @@ -95,8 +103,19 @@ public enum AccessTokenRequestResponse: Codable {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case let .success(accessToken, expiresIn, scope, cNonce, cNonceExpiresIn, _):
case let .success(
tokenType,
accessToken,
refreshToken,
expiresIn,
scope,
cNonce,
cNonceExpiresIn,
_
):
try container.encode(tokenType, forKey: .tokenType)
try container.encode(accessToken, forKey: .accessToken)
try container.encode(refreshToken, forKey: .refreshToken)
try container.encode(expiresIn, forKey: .expiresIn)
try container.encode(scope, forKey: .scope)
try container.encode(cNonce, forKey: .cNonce)
Expand Down
2 changes: 2 additions & 0 deletions Sources/Entities/Types/JWTClaimNames.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ public struct JWTClaimNames {

public extension JWTClaimNames {
static let nonce = "nonce"
static let htm = "htm"
static let htu = "htu"
}
7 changes: 0 additions & 7 deletions Sources/Entities/Types/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,3 @@ public enum InputModeTO: String, Codable {
case text = "text"
case numeric = "numeric"
}

struct TokenResponse: Codable {
let accessToken: AccessToken
let refreshToken: RefreshToken?
let cNonce: CNonce?
let authorizationDetails: [CredentialConfigurationIdentifier: [CredentialIdentifier]]
}
Loading

0 comments on commit 831495d

Please sign in to comment.