blob: 486fdfaa7032bb35d36c78145074701b572f0f0e [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with 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.
package oauth2
import (
"context"
"fmt"
"net/http"
"time"
"github.com/apache/pulsar-client-go/oauth2/clock"
"github.com/pkg/errors"
)
// DeviceCodeFlow takes care of the mechanics needed for getting an access
// token using the OAuth 2.0 "Device Code Flow"
type DeviceCodeFlow struct {
options DeviceCodeFlowOptions
oidcWellKnownEndpoints OIDCWellKnownEndpoints
codeProvider DeviceCodeProvider
exchanger DeviceTokenExchanger
callback DeviceCodeCallback
clock clock.Clock
}
// AuthorizationCodeProvider abstracts getting an authorization code
type DeviceCodeProvider interface {
GetCode(audience string, additionalScopes ...string) (*DeviceCodeResult, error)
}
// DeviceTokenExchanger abstracts exchanging for tokens
type DeviceTokenExchanger interface {
ExchangeDeviceCode(ctx context.Context, req DeviceCodeExchangeRequest) (*TokenResult, error)
ExchangeRefreshToken(req RefreshTokenExchangeRequest) (*TokenResult, error)
}
type DeviceCodeCallback func(code *DeviceCodeResult) error
type DeviceCodeFlowOptions struct {
IssuerEndpoint string
ClientID string
AdditionalScopes []string
AllowRefresh bool
}
func newDeviceCodeFlow(
options DeviceCodeFlowOptions,
oidcWellKnownEndpoints OIDCWellKnownEndpoints,
codeProvider DeviceCodeProvider,
exchanger DeviceTokenExchanger,
callback DeviceCodeCallback,
clock clock.Clock) *DeviceCodeFlow {
return &DeviceCodeFlow{
options: options,
oidcWellKnownEndpoints: oidcWellKnownEndpoints,
codeProvider: codeProvider,
exchanger: exchanger,
callback: callback,
clock: clock,
}
}
// NewDefaultDeviceCodeFlow provides an easy way to build up a default
// device code flow with all the correct configuration. If refresh tokens should
// be allowed pass in true for <allowRefresh>
func NewDefaultDeviceCodeFlow(options DeviceCodeFlowOptions,
callback DeviceCodeCallback) (*DeviceCodeFlow, error) {
wellKnownEndpoints, err := GetOIDCWellKnownEndpointsFromIssuerURL(options.IssuerEndpoint)
if err != nil {
return nil, err
}
codeProvider := NewLocalDeviceCodeProvider(
LocalDeviceCodeProviderOptions{
ClientID: options.ClientID,
},
*wellKnownEndpoints,
&http.Client{},
)
tokenRetriever := NewTokenRetriever(&http.Client{})
return newDeviceCodeFlow(
options,
*wellKnownEndpoints,
codeProvider,
tokenRetriever,
callback,
clock.RealClock{}), nil
}
var _ Flow = &DeviceCodeFlow{}
func (p *DeviceCodeFlow) Authorize(audience string) (*AuthorizationGrant, error) {
var additionalScopes []string
additionalScopes = append(additionalScopes, p.options.AdditionalScopes...)
if p.options.AllowRefresh {
additionalScopes = append(additionalScopes, "offline_access")
}
codeResult, err := p.codeProvider.GetCode(audience, additionalScopes...)
if err != nil {
return nil, err
}
if p.callback != nil {
err := p.callback(codeResult)
if err != nil {
return nil, err
}
}
exchangeRequest := DeviceCodeExchangeRequest{
TokenEndpoint: p.oidcWellKnownEndpoints.TokenEndpoint,
ClientID: p.options.ClientID,
DeviceCode: codeResult.DeviceCode,
PollInterval: time.Duration(codeResult.Interval) * time.Second,
}
tr, err := p.exchanger.ExchangeDeviceCode(context.Background(), exchangeRequest)
if err != nil {
return nil, errors.Wrap(err, "could not exchange code")
}
token := convertToOAuth2Token(tr, p.clock)
grant := &AuthorizationGrant{
Type: GrantTypeDeviceCode,
Audience: audience,
ClientID: p.options.ClientID,
TokenEndpoint: p.oidcWellKnownEndpoints.TokenEndpoint,
Token: &token,
}
return grant, nil
}
type DeviceAuthorizationGrantRefresher struct {
exchanger DeviceTokenExchanger
clock clock.Clock
}
// NewDefaultDeviceAuthorizationGrantRefresher constructs a grant refresher based on the result
// of the device authorization flow.
func NewDefaultDeviceAuthorizationGrantRefresher(clock clock.Clock) (*DeviceAuthorizationGrantRefresher, error) {
tokenRetriever := NewTokenRetriever(&http.Client{})
return &DeviceAuthorizationGrantRefresher{
exchanger: tokenRetriever,
clock: clock,
}, nil
}
var _ AuthorizationGrantRefresher = &DeviceAuthorizationGrantRefresher{}
func (g *DeviceAuthorizationGrantRefresher) Refresh(grant *AuthorizationGrant) (*AuthorizationGrant, error) {
if grant.Type != GrantTypeDeviceCode {
return nil, errors.New("unsupported grant type")
}
if grant.Token == nil || grant.Token.RefreshToken == "" {
return nil, fmt.Errorf("the authorization grant has expired (no refresh token); please re-login")
}
exchangeRequest := RefreshTokenExchangeRequest{
TokenEndpoint: grant.TokenEndpoint,
ClientID: grant.ClientID,
RefreshToken: grant.Token.RefreshToken,
}
tr, err := g.exchanger.ExchangeRefreshToken(exchangeRequest)
if err != nil {
return nil, errors.Wrap(err, "could not exchange refresh token")
}
// RFC 6749 Section 1.5 - token exchange MAY issue a new refresh token (otherwise the result is blank).
// also see: https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-4.12
if tr.RefreshToken == "" {
tr.RefreshToken = grant.Token.RefreshToken
}
token := convertToOAuth2Token(tr, g.clock)
grant = &AuthorizationGrant{
Type: GrantTypeDeviceCode,
Audience: grant.Audience,
ClientID: grant.ClientID,
Token: &token,
TokenEndpoint: grant.TokenEndpoint,
}
return grant, nil
}