api-swift / Sources / IrdAPI / Flows / AuthFlow.swift
309 words
 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
import Foundation

/// Device login (`ird login`) in three phases a native app can drive as state
/// transitions: start (show the code, open the verification URL), wait for the user's
/// approval, then complete with the confirmation code shown in the browser. The
/// resulting access token is an account session: store it with a `CredentialStore`.
extension AuthAPI {
    /// Opens the flow. Show `userCode` and open `verificationUriComplete`.
    public func startDeviceLogin() async throws -> DeviceStartResponse {
        try await deviceStart()
    }

    /// Polls until the user approves. A 400 (`authorization-pending`) and a 429
    /// (`slow-down`) mean "not yet" and let the backoff grow; anything else is rethrown.
    /// `interval` defaults to the server's, `timeout` to the flow's `expires_in`.
    public func waitForDeviceApproval(_ start: DeviceStartResponse, interval: Duration? = nil, timeout: Duration? = nil) async throws {
        let interval = interval ?? .seconds(max(1, start.interval))
        let options = PollOptions(interval: interval, maxInterval: interval * 4,
                                  timeout: timeout ?? .seconds(start.expiresIn),
                                  message: "Timed out waiting for the device login to be approved")
        _ = try await pollUntil(options, clock: client.configuration.clock,
                                step: {
                                    do {
                                        return try await devicePoll(body: DevicePollBody(deviceCode: start.deviceCode)).approved
                                    } catch let e as IrdError where e.status == 400 || e.status == 429 {
                                        return false
                                    }
                                },
                                done: { $0 })
    }

    /// Exchanges the confirmation code the browser showed for the session tokens.
    public func completeDeviceLogin(_ start: DeviceStartResponse, confirmationCode: String) async throws -> DeviceCompleteResponse {
        try await deviceComplete(body: DeviceCompleteBody(deviceCode: start.deviceCode, confirmationCode: confirmationCode))
    }

    /// The composed flow: `prompt` shows the code and URL, `confirmationCode` asks for the
    /// code once approved.
    public func deviceLogin(prompt: @Sendable (DeviceStartResponse) async -> Void,
                            confirmationCode: @Sendable (DeviceStartResponse) async throws -> String,
                            interval: Duration? = nil, timeout: Duration? = nil) async throws -> DeviceCompleteResponse {
        let start = try await startDeviceLogin()
        await prompt(start)
        try await waitForDeviceApproval(start, interval: interval, timeout: timeout)
        return try await completeDeviceLogin(start, confirmationCode: try await confirmationCode(start))
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git