-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathDefaultIDTokenValidator.swift
More file actions
104 lines (98 loc) · 4.2 KB
/
Copy pathDefaultIDTokenValidator.swift
File metadata and controls
104 lines (98 loc) · 4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//
// Copyright (c) 2022-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (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
#if canImport(CommonCrypto)
import CommonCrypto
#endif
struct DefaultIDTokenValidator: IDTokenValidator {
var issuedAtGraceInterval: TimeInterval = 300
var checks: [ValidationCheck] = ValidationCheck.allCases
enum ValidationCheck: CaseIterable {
case issuer, audience, scheme, algorithm, expirationTime, issuedAtTime, nonce, maxAge, subject
}
// swiftlint:disable cyclomatic_complexity
func validate(token: JWT, issuer: URL, clientId: String, context: (any IDTokenValidatorContext)?) throws {
for check in checks {
switch check {
case .issuer:
guard let tokenIssuerString = token.issuer,
let tokenIssuer = URL(string: tokenIssuerString),
tokenIssuer.absoluteString == issuer.absoluteString
else {
throw JWTError.invalidIssuer
}
case .audience:
// RFC 7519 §4.1.3 permits `aud` to be either a string or an
// array of strings. Accept both forms; for the array form,
// require `clientId` to be a member.
if let audienceArray = token.payload[JWTClaim.audience.rawValue] as? [String] {
guard audienceArray.contains(clientId) else {
throw JWTError.invalidAudience
}
} else {
guard token[.audience] == clientId else {
throw JWTError.invalidAudience
}
}
case .scheme:
guard let tokenIssuerString = token.issuer,
let tokenIssuer = URL(string: tokenIssuerString),
tokenIssuer.scheme == "https"
else {
throw JWTError.issuerRequiresHTTPS
}
case .algorithm:
guard token.header.algorithm == .rs256
else {
throw JWTError.unsupportedAlgorithm(token.header.algorithm)
}
case .expirationTime:
guard let expirationTime = token.expirationTime,
expirationTime > Date.nowCoordinated
else {
throw JWTError.expired
}
case .nonce:
guard token["nonce"] == context?.nonce
else {
throw JWTError.nonceMismatch
}
case .issuedAtTime:
guard let issuedAt = token.issuedAt,
abs(issuedAt.timeIntervalSince(Date.nowCoordinated)) <= issuedAtGraceInterval
else {
throw JWTError.issuedAtTimeExceedsGraceInterval
}
case .maxAge:
if let maxAge = context?.maxAge,
let issuedAt = token.issuedAt {
guard let authTime = token.authTime
else {
throw JWTError.invalidAuthenticationTime
}
let elapsedTime = issuedAt.timeIntervalSince(authTime)
guard elapsedTime > 0 && elapsedTime <= maxAge
else {
throw JWTError.exceedsMaxAge
}
}
case .subject:
guard let subject = token.subject,
!subject.isEmpty
else {
throw JWTError.invalidSubject
}
}
}
}
// swiftlint:enable cyclomatic_complexity
}