(ns ird.auth
"Device login flow and API key management. The device endpoints work
anonymously; the API key management endpoints require an account JWT (API
keys cannot manage API keys)."
(:require
[ird.transport :as transport]))
(defn device-start
"POST /auth/device/start — Start a device login. Anonymous allowed.
Returns the parsed response map with :device_code, :user_code,
:verification_uri, :verification_uri_complete, :interval, :expires_in."
[client]
(transport/request client :post "/auth/device/start"))
(defn device-poll
"POST /auth/device/poll — Poll a device login. Anonymous allowed.
Returns the parsed response map with :approved."
[client device-code]
(transport/request client :post "/auth/device/poll"
{:body {:device_code device-code}}))
(defn device-complete
"POST /auth/device/complete — Complete a device login. Anonymous allowed.
Returns the parsed response map with :access_token, :refresh_token, :user."
[client device-code confirmation-code]
(transport/request client :post "/auth/device/complete"
{:body {:device_code device-code
:confirmation_code confirmation-code}}))
(defn list-api-keys
"GET /auth/api-keys — List API keys.
Returns the parsed response map."
[client]
(transport/request client :get "/auth/api-keys"))
(defn create-api-key
"POST /auth/api-keys — Create an API key.
BODY is a plain map with :name.
Returns the parsed response map."
[client body]
(transport/request client :post "/auth/api-keys" {:body body}))
(defn revoke-api-key
"POST /auth/api-keys/{id}/revoke — Revoke an API key.
Returns the parsed response map."
[client id]
(transport/request client :post (str "/auth/api-keys/" id "/revoke")))
(defn device-login
"Convenience: run the whole device login flow.
Starts a device login, hands the start response to ON-PROMPT (default:
prints the verification URL and user code) so a person can approve it in a
browser, then polls until approved and completes the login with
CONFIRMATION-FN (a function of the start response returning the
confirmation code shown to the user after approval; default prompts on
stdin via read-line).
OPTS:
:on-prompt fn of the start response; default prints instructions.
:confirmation-fn fn of the start response returning the confirmation
code; default reads a line from stdin.
:interval-ms poll interval; defaults to the server-suggested
:interval (seconds) from the start response.
:timeout-ms overall deadline; defaults to the server's :expires_in.
Returns the completion map {:access_token :refresh_token :user}; feed
:access_token to ird.client/make-client as :api-key. Throws :ird/timeout
when the login is not approved in time."
([client] (device-login client nil))
([client {:keys [on-prompt confirmation-fn interval-ms timeout-ms]}]
(let [start (device-start client)
on-prompt (or on-prompt
(fn [{:keys [verification_uri_complete verification_uri user_code]}]
(println "To sign in, open:" (or verification_uri_complete verification_uri))
(println "Code:" user_code)))
confirmation-fn (or confirmation-fn
(fn [_]
(println "Enter the confirmation code shown after approving:")
(read-line)))
interval-ms (or interval-ms (* 1000 (or (:interval start) 5)))
timeout-ms (or timeout-ms (* 1000 (or (:expires_in start) 600)))
;; While the user hasn't approved, the API answers with
;; 400 authorization-pending; 429 slow-down asks for a longer
;; interval (poll-until's backoff grows it). Anything else
;; (401 expired, 403 denied) is terminal and rethrown.
poll (fn []
(try
(device-poll client (:device_code start))
(catch clojure.lang.ExceptionInfo e
(let [{:keys [type status]} (ex-data e)]
(if (and (= :ird/api-error type)
(contains? #{400 429} status))
{:approved false}
(throw e))))))]
(on-prompt start)
(transport/poll-until poll :approved
{:interval-ms interval-ms
:timeout-ms timeout-ms
:message "Device login timed out before approval"})
(device-complete client (:device_code start) (confirmation-fn start)))))