Revert "use AuthRequest code flow to create device tokens"

This reverts commit b885398466.
This commit is contained in:
Tim Möhlmann 2023-02-28 12:03:28 +02:00
parent 75f503ce43
commit a80ad6df8a
5 changed files with 139 additions and 94 deletions

View file

@ -28,7 +28,7 @@ type Configuration interface {
EndSessionEndpoint() Endpoint EndSessionEndpoint() Endpoint
KeysEndpoint() Endpoint KeysEndpoint() Endpoint
DeviceAuthorizationEndpoint() Endpoint DeviceAuthorizationEndpoint() Endpoint
UserCodeVerificationEndpoint() Endpoint UserCodeFormEndpoint() Endpoint
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
CodeMethodS256Supported() bool CodeMethodS256Supported() bool

View file

@ -88,7 +88,7 @@ func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvide
VerificationURI: config.UserFormURL, VerificationURI: config.UserFormURL,
} }
endpoint := o.UserCodeVerificationEndpoint().Absolute(IssuerFromContext(r.Context())) endpoint := o.UserCodeFormEndpoint().Absolute(IssuerFromContext(r.Context()))
response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", endpoint, userCode) response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", endpoint, userCode)
httphelper.MarshalJSON(w, response) httphelper.MarshalJSON(w, response)
@ -148,6 +148,24 @@ func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
return buf.String(), nil return buf.String(), nil
} }
type deviceAccessTokenRequest struct {
subject string
audience []string
scopes []string
}
func (r *deviceAccessTokenRequest) GetSubject() string {
return r.subject
}
func (r *deviceAccessTokenRequest) GetAudience() []string {
return r.audience
}
func (r *deviceAccessTokenRequest) GetScopes() []string {
return r.scopes
}
func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
if err := deviceAccessToken(w, r, exchanger); err != nil { if err := deviceAccessToken(w, r, exchanger); err != nil {
RequestError(w, r, err) RequestError(w, r, err)
@ -161,7 +179,7 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang
defer cancel() defer cancel()
r = r.WithContext(ctx) r = r.WithContext(ctx)
clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger) clientID, authenticated, err := ClientIDFromRequest(r, exchanger)
if err != nil { if err != nil {
return err return err
} }
@ -170,7 +188,7 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang
if err != nil { if err != nil {
return err return err
} }
state, authReq, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger) state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
if err != nil { if err != nil {
return err return err
} }
@ -179,14 +197,19 @@ func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchang
if err != nil { if err != nil {
return err return err
} }
if !clientAuthenticated { if !authenticated {
if m := client.AuthMethod(); m != oidc.AuthMethodNone { // Livio: Does this mean "public" client? if m := client.AuthMethod(); m != oidc.AuthMethodNone { // Livio: Does this mean "public" client?
return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials). return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
WithDescription(fmt.Sprintf("required client auth method: %s", m)) WithDescription(fmt.Sprintf("required client auth method: %s", m))
} }
} }
resp, err := CreateTokenResponse(ctx, authReq, client, exchanger, true, state.AuthCode, "") tokenRequest := &deviceAccessTokenRequest{
subject: state.Subject,
audience: []string{clientID},
scopes: state.Scopes,
}
resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
if err != nil { if err != nil {
return err return err
} }
@ -203,88 +226,108 @@ func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.
return req, nil return req, nil
} }
func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, AuthRequest, error) { func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
storage, err := assertDeviceStorage(exchanger.Storage()) storage, err := assertDeviceStorage(exchanger.Storage())
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode) state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
return nil, nil, oidc.ErrSlowDown().WithParent(err) return nil, oidc.ErrSlowDown().WithParent(err)
} }
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
if state.Denied { if state.Denied {
return state, nil, oidc.ErrAccessDenied() return state, oidc.ErrAccessDenied()
} }
if state.AuthCode != "" { if state.Completed {
return state, nil, nil return state, nil
} }
if time.Now().After(state.Expires) { if time.Now().After(state.Expires) {
return state, nil, oidc.ErrExpiredDeviceCode() return state, oidc.ErrExpiredDeviceCode()
} }
authReq, err := AuthRequestByCode(ctx, exchanger.Storage(), state.AuthCode) return state, oidc.ErrAuthorizationPending()
return state, authReq, err
} }
func userCodeVerificationHandler(o OpenIDProvider) http.HandlerFunc { func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) {
return func(w http.ResponseWriter, r *http.Request) { tokenType := AccessTokenTypeBearer // not sure if this is the correct type?
UserCodeVerification(w, r, o)
}
}
type UserCodeVerificationRequest struct { accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, tokenType, creator, client, "")
Code string `schema:"code"`
UserCode string `schema:"user_code"`
RedirectURL string `schema:"redirect_url"`
}
func UserCodeVerification(w http.ResponseWriter, r *http.Request, o OpenIDProvider) {
if err := userCodeVerification(w, r, o); err != nil {
RequestError(w, r, err)
}
}
func userCodeVerification(w http.ResponseWriter, r *http.Request, o OpenIDProvider) (err error) {
req, err := ParseUserCodeVerificationRequest(r, o.Decoder())
if err != nil { if err != nil {
return err return nil, err
}
return &oidc.AccessTokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: oidc.BearerToken,
ExpiresIn: uint64(validity.Seconds()),
}, nil
}
func userCodeFormHandler(o OpenIDProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
UserCodeForm(w, r, o)
}
}
type UserCodeFormData struct {
AccesssToken string `schema:"access_token"`
UserCode string `schema:"user_code"`
RedirectURL string `schema:"redirect_url"`
}
func UserCodeForm(w http.ResponseWriter, r *http.Request, o OpenIDProvider) {
data, err := ParseUserCodeFormData(r, o.Decoder())
if err != nil {
RequestError(w, r, err)
return
} }
storage, err := assertDeviceStorage(o.Storage()) storage, err := assertDeviceStorage(o.Storage())
if err != nil { if err != nil {
return err RequestError(w, r, err)
return
} }
ctx := r.Context() ctx := r.Context()
if err := storage.CompleteDeviceAuthorization(ctx, req.Code, req.UserCode); err != nil { token, err := VerifyAccessToken(ctx, data.AccesssToken, o.AccessTokenVerifier(ctx))
return err if err != nil {
if se := storage.DenyDeviceAuthorization(ctx, data.UserCode); se != nil {
err = se
}
RequestError(w, r, err)
return
} }
if req.RedirectURL != "" { if err := storage.CompleteDeviceAuthorization(ctx, data.UserCode, token.GetSubject()); err != nil {
http.Redirect(w, r, req.RedirectURL, http.StatusSeeOther) RequestError(w, r, err)
return
}
if data.RedirectURL != "" {
http.Redirect(w, r, data.RedirectURL, http.StatusSeeOther)
} }
fmt.Fprintln(w, "Authorization successfull, please return to your device") fmt.Fprintln(w, "Authorization successfull, please return to your device")
return nil
} }
func ParseUserCodeVerificationRequest(r *http.Request, decoder httphelper.Decoder) (*UserCodeVerificationRequest, error) { func ParseUserCodeFormData(r *http.Request, decoder httphelper.Decoder) (*UserCodeFormData, error) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err) return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
} }
req := new(UserCodeVerificationRequest) req := new(UserCodeFormData)
if err := decoder.Decode(req, r.Form); err != nil { if err := decoder.Decode(req, r.Form); err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse user code form").WithParent(err) return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse user code form").WithParent(err)
} }
if req.Code == "" { if req.AccesssToken == "" {
return nil, oidc.ErrInvalidRequest().WithDescription("\"code\" missing in form") return nil, oidc.ErrInvalidRequest().WithDescription("access_token missing in form")
} }
if req.UserCode == "" { if req.UserCode == "" {
return nil, oidc.ErrInvalidRequest().WithDescription("\"user_code\" missing in form") return nil, oidc.ErrInvalidRequest().WithDescription("user_code missing in form")
} }
return req, nil return req, nil

View file

@ -400,18 +400,18 @@ func (mr *MockConfigurationMockRecorder) TokenEndpointSigningAlgorithmsSupported
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpointSigningAlgorithmsSupported)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).TokenEndpointSigningAlgorithmsSupported))
} }
// UserCodeVerificationEndpoint mocks base method. // UserCodeFormEndpoint mocks base method.
func (m *MockConfiguration) UserCodeVerificationEndpoint() op.Endpoint { func (m *MockConfiguration) UserCodeFormEndpoint() op.Endpoint {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserCodeVerificationEndpoint") ret := m.ctrl.Call(m, "UserCodeFormEndpoint")
ret0, _ := ret[0].(op.Endpoint) ret0, _ := ret[0].(op.Endpoint)
return ret0 return ret0
} }
// UserCodeVerificationEndpoint indicates an expected call of UserCodeVerificationEndpoint. // UserCodeFormEndpoint indicates an expected call of UserCodeFormEndpoint.
func (mr *MockConfigurationMockRecorder) UserCodeVerificationEndpoint() *gomock.Call { func (mr *MockConfigurationMockRecorder) UserCodeFormEndpoint() *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeVerificationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).UserCodeVerificationEndpoint)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCodeFormEndpoint", reflect.TypeOf((*MockConfiguration)(nil).UserCodeFormEndpoint))
} }
// UserinfoEndpoint mocks base method. // UserinfoEndpoint mocks base method.

View file

@ -17,31 +17,31 @@ import (
) )
const ( const (
healthEndpoint = "/healthz" healthEndpoint = "/healthz"
readinessEndpoint = "/ready" readinessEndpoint = "/ready"
authCallbackPathSuffix = "/callback" authCallbackPathSuffix = "/callback"
defaultAuthorizationEndpoint = "authorize" defaultAuthorizationEndpoint = "authorize"
defaultTokenEndpoint = "oauth/token" defaultTokenEndpoint = "oauth/token"
defaultIntrospectEndpoint = "oauth/introspect" defaultIntrospectEndpoint = "oauth/introspect"
defaultUserinfoEndpoint = "userinfo" defaultUserinfoEndpoint = "userinfo"
defaultRevocationEndpoint = "revoke" defaultRevocationEndpoint = "revoke"
defaultEndSessionEndpoint = "end_session" defaultEndSessionEndpoint = "end_session"
defaultKeysEndpoint = "keys" defaultKeysEndpoint = "keys"
defaultDeviceAuthzEndpoint = "/device_authorization" defaultDeviceAuthzEndpoint = "/device_authorization"
defaultUserCodeVerificationEndpoint = "/user_code" defaultUserCodeFormEndpoint = "/submit_user_code"
) )
var ( var (
DefaultEndpoints = &endpoints{ DefaultEndpoints = &endpoints{
Authorization: NewEndpoint(defaultAuthorizationEndpoint), Authorization: NewEndpoint(defaultAuthorizationEndpoint),
Token: NewEndpoint(defaultTokenEndpoint), Token: NewEndpoint(defaultTokenEndpoint),
Introspection: NewEndpoint(defaultIntrospectEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint),
Userinfo: NewEndpoint(defaultUserinfoEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint),
Revocation: NewEndpoint(defaultRevocationEndpoint), Revocation: NewEndpoint(defaultRevocationEndpoint),
EndSession: NewEndpoint(defaultEndSessionEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint),
JwksURI: NewEndpoint(defaultKeysEndpoint), JwksURI: NewEndpoint(defaultKeysEndpoint),
DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint), DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint),
UserCodeVerification: NewEndpoint(defaultUserCodeVerificationEndpoint), UserCodeForm: NewEndpoint(defaultUserCodeFormEndpoint),
} }
defaultCORSOptions = cors.Options{ defaultCORSOptions = cors.Options{
@ -100,7 +100,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o)) router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o))
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage())) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), deviceAuthorizationHandler(o)) router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), deviceAuthorizationHandler(o))
router.HandleFunc(o.UserCodeVerificationEndpoint().Relative(), userCodeVerificationHandler(o)) router.HandleFunc(o.UserCodeFormEndpoint().Relative(), userCodeFormHandler(o))
return router return router
} }
@ -128,16 +128,16 @@ type Config struct {
} }
type endpoints struct { type endpoints struct {
Authorization Endpoint Authorization Endpoint
Token Endpoint Token Endpoint
Introspection Endpoint Introspection Endpoint
Userinfo Endpoint Userinfo Endpoint
Revocation Endpoint Revocation Endpoint
EndSession Endpoint EndSession Endpoint
CheckSessionIframe Endpoint CheckSessionIframe Endpoint
JwksURI Endpoint JwksURI Endpoint
DeviceAuthorization Endpoint DeviceAuthorization Endpoint
UserCodeVerification Endpoint UserCodeForm Endpoint
} }
// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) // NewOpenIDProvider creates a provider. The provider provides (with HttpHandler())
@ -256,8 +256,8 @@ func (o *Provider) DeviceAuthorizationEndpoint() Endpoint {
return o.endpoints.DeviceAuthorization return o.endpoints.DeviceAuthorization
} }
func (o *Provider) UserCodeVerificationEndpoint() Endpoint { func (o *Provider) UserCodeFormEndpoint() Endpoint {
return o.endpoints.UserCodeVerification return o.endpoints.UserCodeForm
} }
func (o *Provider) KeysEndpoint() Endpoint { func (o *Provider) KeysEndpoint() Endpoint {

View file

@ -155,10 +155,11 @@ type EndSessionRequest struct {
var ErrDuplicateUserCode = errors.New("user code already exists") var ErrDuplicateUserCode = errors.New("user code already exists")
type DeviceAuthorizationState struct { type DeviceAuthorizationState struct {
Scopes []string Scopes []string
Expires time.Time Expires time.Time
AuthCode string Completed bool
Denied bool Subject string
Denied bool
} }
type DeviceAuthorizationStorage interface { type DeviceAuthorizationStorage interface {
@ -176,9 +177,10 @@ type DeviceAuthorizationStorage interface {
// The method is polled untill the the authorization is eighter Completed, Expired or Denied. // The method is polled untill the the authorization is eighter Completed, Expired or Denied.
GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error)
// CompleteDeviceAuthorization marks a device authorization entry identified by userCode // CompleteDeviceAuthorization marks a device authorization entry as Completed,
// as completed, by setting the related authCode from an AuthRequest. // identified by userCode. The Subject is added to the state, so that
CompleteDeviceAuthorization(ctx context.Context, authCode, userCode string) error // GetDeviceAuthorizatonState can use it to create a new Access Token.
CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error
// DenyDeviceAuthorization marks a device authorization entry as Denied. // DenyDeviceAuthorization marks a device authorization entry as Denied.
DenyDeviceAuthorization(ctx context.Context, userCode string) error DenyDeviceAuthorization(ctx context.Context, userCode string) error