* feat: extend token exchange response This change adds fields to the token exchange and token claims types. The `act` claim has been added to describe the actor in case of impersonation or delegation. An actor can be nested in case an obtained token is used as actor token to obtain impersonation or delegation. This allows creating a chain of actors. See [RFC 8693, section 4.1](https://www.rfc-editor.org/rfc/rfc8693#name-act-actor-claim). The `id_token` field has been added to the Token Exchange response so an ID Token can be returned along with an access token. This is not specified in RFC 8693, but it allows us be consistent with OpenID responses when the scope `openid` is set, while the requested token type may remain access token. * allow jwt profile for token exchange client * add invalid target error
218 lines
5.4 KiB
Go
218 lines
5.4 KiB
Go
package oidc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
)
|
|
|
|
type errorType string
|
|
|
|
const (
|
|
InvalidRequest errorType = "invalid_request"
|
|
InvalidScope errorType = "invalid_scope"
|
|
InvalidClient errorType = "invalid_client"
|
|
InvalidGrant errorType = "invalid_grant"
|
|
UnauthorizedClient errorType = "unauthorized_client"
|
|
UnsupportedGrantType errorType = "unsupported_grant_type"
|
|
ServerError errorType = "server_error"
|
|
InteractionRequired errorType = "interaction_required"
|
|
LoginRequired errorType = "login_required"
|
|
RequestNotSupported errorType = "request_not_supported"
|
|
|
|
// Additional error codes as defined in
|
|
// https://www.rfc-editor.org/rfc/rfc8628#section-3.5
|
|
// Device Access Token Response
|
|
AuthorizationPending errorType = "authorization_pending"
|
|
SlowDown errorType = "slow_down"
|
|
AccessDenied errorType = "access_denied"
|
|
ExpiredToken errorType = "expired_token"
|
|
|
|
// InvalidTarget error is returned by Token Exchange if
|
|
// the requested target or audience is invalid.
|
|
// [RFC 8693, Section 2.2.2: Error Response](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.2)
|
|
InvalidTarget errorType = "invalid_target"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidRequest = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidRequest,
|
|
}
|
|
}
|
|
ErrInvalidRequestRedirectURI = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidRequest,
|
|
redirectDisabled: true,
|
|
}
|
|
}
|
|
ErrInvalidScope = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidScope,
|
|
}
|
|
}
|
|
ErrInvalidClient = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidClient,
|
|
}
|
|
}
|
|
ErrInvalidGrant = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidGrant,
|
|
}
|
|
}
|
|
ErrUnauthorizedClient = func() *Error {
|
|
return &Error{
|
|
ErrorType: UnauthorizedClient,
|
|
}
|
|
}
|
|
ErrUnsupportedGrantType = func() *Error {
|
|
return &Error{
|
|
ErrorType: UnsupportedGrantType,
|
|
}
|
|
}
|
|
ErrServerError = func() *Error {
|
|
return &Error{
|
|
ErrorType: ServerError,
|
|
}
|
|
}
|
|
ErrInteractionRequired = func() *Error {
|
|
return &Error{
|
|
ErrorType: InteractionRequired,
|
|
}
|
|
}
|
|
ErrLoginRequired = func() *Error {
|
|
return &Error{
|
|
ErrorType: LoginRequired,
|
|
}
|
|
}
|
|
ErrRequestNotSupported = func() *Error {
|
|
return &Error{
|
|
ErrorType: RequestNotSupported,
|
|
}
|
|
}
|
|
|
|
// Device Access Token errors:
|
|
ErrAuthorizationPending = func() *Error {
|
|
return &Error{
|
|
ErrorType: AuthorizationPending,
|
|
Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.",
|
|
}
|
|
}
|
|
ErrSlowDown = func() *Error {
|
|
return &Error{
|
|
ErrorType: SlowDown,
|
|
Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.",
|
|
}
|
|
}
|
|
ErrAccessDenied = func() *Error {
|
|
return &Error{
|
|
ErrorType: AccessDenied,
|
|
Description: "The authorization request was denied.",
|
|
}
|
|
}
|
|
ErrExpiredDeviceCode = func() *Error {
|
|
return &Error{
|
|
ErrorType: ExpiredToken,
|
|
Description: "The \"device_code\" has expired.",
|
|
}
|
|
}
|
|
|
|
// Token exchange error
|
|
ErrInvalidTarget = func() *Error {
|
|
return &Error{
|
|
ErrorType: InvalidTarget,
|
|
Description: "The requested audience or target is invalid.",
|
|
}
|
|
}
|
|
)
|
|
|
|
type Error struct {
|
|
Parent error `json:"-" schema:"-"`
|
|
ErrorType errorType `json:"error" schema:"error"`
|
|
Description string `json:"error_description,omitempty" schema:"error_description,omitempty"`
|
|
State string `json:"state,omitempty" schema:"state,omitempty"`
|
|
redirectDisabled bool `schema:"-"`
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
message := "ErrorType=" + string(e.ErrorType)
|
|
if e.Description != "" {
|
|
message += " Description=" + e.Description
|
|
}
|
|
if e.Parent != nil {
|
|
message += " Parent=" + e.Parent.Error()
|
|
}
|
|
return message
|
|
}
|
|
|
|
func (e *Error) Unwrap() error {
|
|
return e.Parent
|
|
}
|
|
|
|
func (e *Error) Is(target error) bool {
|
|
t, ok := target.(*Error)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return e.ErrorType == t.ErrorType &&
|
|
(e.Description == t.Description || t.Description == "") &&
|
|
(e.State == t.State || t.State == "")
|
|
}
|
|
|
|
func (e *Error) WithParent(err error) *Error {
|
|
e.Parent = err
|
|
return e
|
|
}
|
|
|
|
func (e *Error) WithDescription(desc string, args ...any) *Error {
|
|
e.Description = fmt.Sprintf(desc, args...)
|
|
return e
|
|
}
|
|
|
|
func (e *Error) IsRedirectDisabled() bool {
|
|
return e.redirectDisabled
|
|
}
|
|
|
|
// DefaultToServerError checks if the error is an Error
|
|
// if not the provided error will be wrapped into a ServerError
|
|
func DefaultToServerError(err error, description string) *Error {
|
|
oauth := new(Error)
|
|
if ok := errors.As(err, &oauth); !ok {
|
|
oauth.ErrorType = ServerError
|
|
oauth.Description = description
|
|
oauth.Parent = err
|
|
}
|
|
return oauth
|
|
}
|
|
|
|
func (e *Error) LogLevel() slog.Level {
|
|
level := slog.LevelWarn
|
|
if e.ErrorType == ServerError {
|
|
level = slog.LevelError
|
|
}
|
|
if e.ErrorType == AuthorizationPending {
|
|
level = slog.LevelInfo
|
|
}
|
|
return level
|
|
}
|
|
|
|
func (e *Error) LogValue() slog.Value {
|
|
attrs := make([]slog.Attr, 0, 5)
|
|
if e.Parent != nil {
|
|
attrs = append(attrs, slog.Any("parent", e.Parent))
|
|
}
|
|
if e.Description != "" {
|
|
attrs = append(attrs, slog.String("description", e.Description))
|
|
}
|
|
if e.ErrorType != "" {
|
|
attrs = append(attrs, slog.String("type", string(e.ErrorType)))
|
|
}
|
|
if e.State != "" {
|
|
attrs = append(attrs, slog.String("state", e.State))
|
|
}
|
|
if e.redirectDisabled {
|
|
attrs = append(attrs, slog.Bool("redirect_disabled", e.redirectDisabled))
|
|
}
|
|
return slog.GroupValue(attrs...)
|
|
}
|