main
swift 99 lines 3.14 KB
Raw
1 import Foundation
2
3 /// A persisted credential — the access token plus optional refresh/id tokens
4 /// and expiry. Produced by the OIDC token exchange (and storable from a tenant
5 /// login), and what ``SmbCloudCredentialsStore`` persists.
6 public struct SmbCloudSession: Codable, Equatable, Hashable, Sendable {
7 public let accessToken: String
8 public let refreshToken: String?
9 public let idToken: String?
10 public let tokenType: String
11 public let expiresAt: Date?
12 public let scopes: [String]
13
14 public init(
15 accessToken: String,
16 refreshToken: String? = nil,
17 idToken: String? = nil,
18 tokenType: String = "Bearer",
19 expiresAt: Date? = nil,
20 scopes: [String] = []
21 ) {
22 self.accessToken = accessToken
23 self.refreshToken = refreshToken
24 self.idToken = idToken
25 self.tokenType = tokenType
26 self.expiresAt = expiresAt
27 self.scopes = scopes
28 }
29
30 public var scope: String? {
31 scopes.isEmpty ? nil : scopes.joined(separator: " ")
32 }
33
34 public var authorizationHeaderValue: String {
35 "\(tokenType) \(accessToken)"
36 }
37
38 public var isExpired: Bool {
39 isExpired(leeway: 0)
40 }
41
42 public func isExpired(leeway: TimeInterval = 0) -> Bool {
43 guard let expiresAt else { return false }
44 return expiresAt <= Date().addingTimeInterval(leeway)
45 }
46
47 public func isValid(leeway: TimeInterval = 60) -> Bool {
48 isExpired(leeway: leeway) == false
49 }
50 }
51
52 extension SmbCloudSession {
53 /// Builds a session from an OIDC token exchange response.
54 public init(tokenResponse: OIDC.TokenResponse, now: Date = Date()) {
55 let expiresAt = tokenResponse.expiresIn.map { now.addingTimeInterval(TimeInterval($0)) }
56 let scopes =
57 tokenResponse.scope?
58 .split(separator: " ")
59 .map(String.init)
60 .smbCloudUniqued() ?? []
61 self.init(
62 accessToken: tokenResponse.accessToken,
63 refreshToken: tokenResponse.refreshToken?.smbCloudNilIfEmpty,
64 idToken: tokenResponse.idToken?.smbCloudNilIfEmpty,
65 tokenType: tokenResponse.tokenType,
66 expiresAt: expiresAt,
67 scopes: scopes
68 )
69 }
70
71 /// A normalized copy (trimmed/deduped scopes, empty tokens dropped).
72 public func storedRepresentation() -> SmbCloudSession {
73 SmbCloudSession(
74 accessToken: accessToken,
75 refreshToken: refreshToken?.smbCloudNilIfEmpty,
76 idToken: idToken?.smbCloudNilIfEmpty,
77 tokenType: tokenType,
78 expiresAt: expiresAt,
79 scopes: scopes.smbCloudUniqued()
80 )
81 }
82 }
83
84 extension String {
85 var smbCloudNilIfEmpty: String? {
86 trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
87 }
88 }
89
90 extension Sequence where Element == String {
91 func smbCloudUniqued() -> [String] {
92 var seen = Set<String>()
93 return compactMap { value in
94 let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
95 guard !trimmed.isEmpty else { return nil }
96 return seen.insert(trimmed).inserted ? trimmed : nil
97 }
98 }
99 }