@@ -32,27 +32,28 @@ const invalidDeviceCodeDescription = "Invalid device code"
3232// a device authorization endpoint in its OIDC discovery document.
3333var ErrDeviceCodeNotSupported = errors .New ("device code login is not enabled for this workspace" )
3434
35+ // ErrInvalidConfirmationCode matches the token exchange error for a
36+ // confirmation code the server rejects. The device code stays valid, so the
37+ // exchange can be retried with the right code.
38+ var ErrInvalidConfirmationCode = errors .New ("invalid confirmation code" )
39+
40+ // ErrInvalidDeviceCode matches the token exchange error for a device code the
41+ // server does not know, e.g. one it discarded after the login expired. The
42+ // login has to be started again.
43+ var ErrInvalidDeviceCode = errors .New ("invalid device code" )
44+
3545// DeviceAuthorization holds what the user needs to approve a device code
36- // login from another device.
46+ // login from another device, and the device code to exchange afterwards .
3747type DeviceAuthorization struct {
3848 // UserCode is the code the user enters at VerificationURI.
3949 UserCode string
4050
4151 // VerificationURI is the page where the user approves the login.
4252 VerificationURI string
43- }
4453
45- // maxConfirmationAttempts is how many confirmation codes the user can enter
46- // for a single device code before the login fails.
47- const maxConfirmationAttempts = 3
48-
49- // ConfirmFunc shows the device authorization to the user and returns the
50- // confirmation code that the user received after approving the login. When
51- // the server rejects a confirmation code, it is called again with the
52- // server's error in lastErr so that the user can re-enter the code. lastErr
53- // is nil on the first call. attemptsLeft is the number of confirmation codes
54- // the user can still enter, including this one.
55- type ConfirmFunc func (ctx context.Context , da DeviceAuthorization , lastErr error , attemptsLeft int ) (string , error )
54+ // DeviceCode identifies the login in the token exchange.
55+ DeviceCode string
56+ }
5657
5758// deviceCodeConfig returns the OAuth config for device code login, or an
5859// error if the host does not support it.
@@ -75,53 +76,83 @@ func (a *PersistentAuth) deviceCodeConfig() (*oauth2.Config, error) {
7576 return cfg , nil
7677}
7778
78- // DeviceCodeChallenge logs in using the Databricks variant of the RFC 8628
79- // device authorization grant. It requests a device authorization, calls
80- // confirm with the user code and verification URI, and exchanges the device
81- // code together with the returned confirmation code for a token. A rejected
82- // confirmation code leaves the device code valid, so the user can re-enter
83- // it up to maxConfirmationAttempts times. Only workspace hosts are supported.
84- // The caller is responsible for storing the returned token.
85- func (a * PersistentAuth ) DeviceCodeChallenge (ctx context.Context , confirm ConfirmFunc ) (* oauth2.Token , error ) {
79+ // DeviceCodeLogin makes the requests of a login using the Databricks variant
80+ // of the RFC 8628 device authorization grant. Get one with
81+ // PersistentAuth.DeviceCodeLogin.
82+ type DeviceCodeLogin struct {
83+ auth * PersistentAuth
84+ cfg * oauth2.Config
85+ }
86+
87+ // DeviceCodeLogin fetches the OAuth endpoints for a device code login, or
88+ // returns an error if the host does not support it. Only workspace hosts are
89+ // supported.
90+ func (a * PersistentAuth ) DeviceCodeLogin () (* DeviceCodeLogin , error ) {
8691 cfg , err := a .deviceCodeConfig ()
8792 if err != nil {
8893 return nil , err
8994 }
95+ return & DeviceCodeLogin {auth : a , cfg : cfg }, nil
96+ }
9097
91- da , err := a .requestDeviceAuthorization (ctx , cfg )
98+ // RequestDeviceAuthorization starts the login. The caller shows the user code
99+ // and the verification URI to the user and later passes the device code to
100+ // RequestDeviceAccessToken.
101+ func (l * DeviceCodeLogin ) RequestDeviceAuthorization (ctx context.Context ) (* DeviceAuthorization , error ) {
102+ da , err := l .auth .requestDeviceAuthorization (ctx , l .cfg )
92103 if err != nil {
93104 return nil , fmt .Errorf ("device authorization: %w" , oauthError (err ))
94105 }
95-
96- authorization := DeviceAuthorization {
106+ return & DeviceAuthorization {
97107 UserCode : da .UserCode ,
98108 VerificationURI : da .VerificationURI ,
99- }
100- var lastErr error
101- for attempt := range maxConfirmationAttempts {
102- code , err := confirm (ctx , authorization , lastErr , maxConfirmationAttempts - attempt )
103- if err != nil {
104- return nil , err
105- }
109+ DeviceCode : da .DeviceCode ,
110+ }, nil
111+ }
106112
107- t , err := a .exchangeDeviceCode (ctx , cfg , da .DeviceCode , normalizeCode (code ))
108- if err == nil {
109- return t , nil
110- }
111- lastErr = oauthError (err )
112- // The server answers a wrong confirmation code with HTTP 400 and
113- // invalid_request. Any other error cannot be fixed by entering the
114- // code again, including invalid_request with HTTP 429 when the server
115- // rate limits the token requests for this device code. An unknown
116- // device code also gets HTTP 400 and invalid_request, and only the
117- // description tells it apart. If the description changes, the user is
118- // asked for the code again until the attempts run out.
119- status , errCode , description := oauthErrorResponse (err )
120- if status != http .StatusBadRequest || errCode != "invalid_request" || description == invalidDeviceCodeDescription {
121- return nil , fmt .Errorf ("token exchange: %w" , lastErr )
122- }
113+ // RequestDeviceAccessToken completes the login with a single token request.
114+ // The confirmation code is accepted the way the browser displays it. An error
115+ // matches ErrInvalidConfirmationCode or ErrInvalidDeviceCode when the server
116+ // rejects one of the codes. A rejected confirmation code leaves the device
117+ // code valid, so the exchange can be retried with the right code. The caller
118+ // is responsible for storing the returned token.
119+ func (l * DeviceCodeLogin ) RequestDeviceAccessToken (ctx context.Context , deviceCode , confirmationCode string ) (* oauth2.Token , error ) {
120+ t , err := l .auth .exchangeDeviceCode (ctx , l .cfg , deviceCode , normalizeCode (confirmationCode ))
121+ if err == nil {
122+ return t , nil
123+ }
124+ // The server answers a wrong confirmation code with HTTP 400 and
125+ // invalid_request. Any other error cannot be fixed by entering the
126+ // code again, including invalid_request with HTTP 429 when the server
127+ // rate limits the token requests for this device code. An unknown
128+ // device code also gets HTTP 400 and invalid_request, and only the
129+ // description tells it apart. If the description changes, the error is
130+ // reported as a wrong confirmation code.
131+ status , errCode , description := oauthErrorResponse (err )
132+ err = oauthError (err )
133+ switch {
134+ case status != http .StatusBadRequest || errCode != "invalid_request" :
135+ return nil , err
136+ case description == invalidDeviceCodeDescription :
137+ return nil , & codeRejectedError {err : err , sentinel : ErrInvalidDeviceCode }
138+ default :
139+ return nil , & codeRejectedError {err : err , sentinel : ErrInvalidConfirmationCode }
123140 }
124- return nil , fmt .Errorf ("%w. Start login from scratch" , lastErr )
141+ }
142+
143+ // codeRejectedError shows the server's error for a rejected code and matches
144+ // sentinel with errors.Is.
145+ type codeRejectedError struct {
146+ err error
147+ sentinel error
148+ }
149+
150+ func (e * codeRejectedError ) Error () string {
151+ return e .err .Error ()
152+ }
153+
154+ func (e * codeRejectedError ) Unwrap () []error {
155+ return []error {e .err , e .sentinel }
125156}
126157
127158// requestDeviceAuthorization makes the RFC 8628 device authorization request.
0 commit comments