first draft of a new server interface
This commit is contained in:
parent
daf82a5e04
commit
d6a9c0bbb9
9 changed files with 481 additions and 9 deletions
|
@ -180,3 +180,11 @@ func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, au
|
||||||
}
|
}
|
||||||
return data.ClientID, false, nil
|
return data.ClientID, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientCredentials struct {
|
||||||
|
ClientID string `schema:"client_id"`
|
||||||
|
ClientSecret string `schema:"client_secret"` // Client secret from request body
|
||||||
|
ClientSecretBasic string `schema:"-"` // Obtained from http request
|
||||||
|
ClientAssertion string `schema:"client_assertion"` // JWT
|
||||||
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultEndpoints = &endpoints{
|
DefaultEndpoints = &Endpoints{
|
||||||
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
|
||||||
Token: NewEndpoint(defaultTokenEndpoint),
|
Token: NewEndpoint(defaultTokenEndpoint),
|
||||||
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
Introspection: NewEndpoint(defaultIntrospectEndpoint),
|
||||||
|
@ -131,7 +131,7 @@ type Config struct {
|
||||||
DeviceAuthorization DeviceAuthorizationConfig
|
DeviceAuthorization DeviceAuthorizationConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type endpoints struct {
|
type Endpoints struct {
|
||||||
Authorization Endpoint
|
Authorization Endpoint
|
||||||
Token Endpoint
|
Token Endpoint
|
||||||
Introspection Endpoint
|
Introspection Endpoint
|
||||||
|
@ -212,7 +212,7 @@ type Provider struct {
|
||||||
config *Config
|
config *Config
|
||||||
issuer IssuerFromRequest
|
issuer IssuerFromRequest
|
||||||
insecure bool
|
insecure bool
|
||||||
endpoints *endpoints
|
endpoints *Endpoints
|
||||||
storage Storage
|
storage Storage
|
||||||
keySet *openIDKeySet
|
keySet *openIDKeySet
|
||||||
crypto Crypto
|
crypto Crypto
|
||||||
|
|
|
@ -41,9 +41,9 @@ func ReadyStorage(s Storage) ProbesFn {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ok(w http.ResponseWriter) {
|
func ok(w http.ResponseWriter) {
|
||||||
httphelper.MarshalJSON(w, status{"ok"})
|
httphelper.MarshalJSON(w, Status{"ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
type status struct {
|
type Status struct {
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
269
pkg/op/server.go
Normal file
269
pkg/op/server.go
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
jose "github.com/go-jose/go-jose/v3"
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusError struct {
|
||||||
|
parent error
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatusError(parent error, statusCode int) StatusError {
|
||||||
|
return StatusError{
|
||||||
|
parent: parent,
|
||||||
|
statusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", http.StatusText(e.statusCode), e.parent.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusError) Unwrap() error {
|
||||||
|
return e.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusError) Is(err error) bool {
|
||||||
|
var target StatusError
|
||||||
|
if !errors.As(err, &target) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return errors.Is(e.parent, target.parent) &&
|
||||||
|
e.statusCode == target.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request[T any] struct {
|
||||||
|
Method string
|
||||||
|
URL *url.URL
|
||||||
|
Header http.Header
|
||||||
|
Form url.Values
|
||||||
|
Data *T
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest[T any](r *http.Request, data *T) *Request[T] {
|
||||||
|
return &Request[T]{
|
||||||
|
Method: r.Method,
|
||||||
|
URL: r.URL,
|
||||||
|
Header: r.Header,
|
||||||
|
Form: r.Form,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientRequest[T any] struct {
|
||||||
|
*Request[T]
|
||||||
|
Client Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientRequest[T any](r *http.Request, data *T, client Client) *ClientRequest[T] {
|
||||||
|
return &ClientRequest[T]{
|
||||||
|
Request: newRequest[T](r, data),
|
||||||
|
Client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response[T any] struct {
|
||||||
|
Header http.Header
|
||||||
|
Data *T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse[T any](data *T) *Response[T] {
|
||||||
|
return &Response[T]{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resp *Response[T]) writeOut(w http.ResponseWriter) {
|
||||||
|
gu.MapMerge(resp.Header, w.Header())
|
||||||
|
json.NewEncoder(w).Encode(resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server interface {
|
||||||
|
// Health should return a status of "ok" once the Server is listining.
|
||||||
|
Health(context.Context, *Request[struct{}]) (*Response[Status], error)
|
||||||
|
|
||||||
|
// Ready should return a status of "ok" once all dependecies,
|
||||||
|
// such as database storage are ready.
|
||||||
|
// An error can be returned to explain what is not ready.
|
||||||
|
Ready(context.Context, *Request[struct{}]) (*Response[Status], error)
|
||||||
|
|
||||||
|
// Discovery return the OpenID Provider Configuration Information for this server.
|
||||||
|
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
|
||||||
|
Discovery(context.Context, *Request[struct{}]) (*Response[oidc.DiscoveryConfiguration], error)
|
||||||
|
|
||||||
|
// Authorize initiates the authorization flow and redirects to a login page.
|
||||||
|
// See the various https://openid.net/specs/openid-connect-core-1_0.html
|
||||||
|
// authorize endpoint sections (one for each type of flow).
|
||||||
|
Authorize(context.Context, *Request[oidc.AuthRequest]) (*Response[url.URL], error)
|
||||||
|
|
||||||
|
// AuthorizeCallback? Do we still need it?
|
||||||
|
|
||||||
|
// DeviceAuthorization initiates the device authorization flow.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
|
||||||
|
DeviceAuthorization(context.Context, *Request[oidc.DeviceAuthorizationRequest]) (*Response[oidc.DeviceAuthorizationResponse], error)
|
||||||
|
|
||||||
|
// VerifyClient is called on most oauth/token handlers to authenticate,
|
||||||
|
// using either a secret (POST, Basic) or assertion (JWT).
|
||||||
|
// If no secrets are provided, the client must be public.
|
||||||
|
// This method is called before each method that takes a
|
||||||
|
// [ClientRequest] argument.
|
||||||
|
VerifyClient(context.Context, *Request[ClientCredentials]) (Client, error)
|
||||||
|
|
||||||
|
// CodeExchange returns Tokens after an authorization code
|
||||||
|
// is obtained in a succesfull Authorize flow.
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value authorization_code
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||||
|
CodeExchange(context.Context, *ClientRequest[oidc.AccessTokenRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// RefreshToken returns new Tokens after verifying a Refresh token.
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value refresh_token
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
||||||
|
RefreshToken(context.Context, *ClientRequest[oidc.RefreshTokenRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
|
||||||
|
JWTProfile(context.Context, *Request[oidc.JWTProfileGrantRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// TokenExchange handles the OAuth 2.0 token exchange grant
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value urn:ietf:params:oauth:grant-type:token-exchange
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8693
|
||||||
|
TokenExchange(context.Context, *ClientRequest[oidc.TokenExchangeRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// ClientCredentialsExchange handles the OAuth 2.0 client credentials grant
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value client_credentials
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
||||||
|
ClientCredentialsExchange(context.Context, *ClientRequest[oidc.ClientCredentialsRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// DeviceToken handles the OAuth 2.0 Device Authorization Grant
|
||||||
|
// It is called by the Token endpoint handler when
|
||||||
|
// grant_type has the value urn:ietf:params:oauth:grant-type:device_code.
|
||||||
|
// It is typically called in a polling fashion and appropiate errors
|
||||||
|
// should be returned to signal authorization_pending or access_denied etc.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.4,
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5.
|
||||||
|
DeviceToken(context.Context, *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response[oidc.AccessTokenResponse], error)
|
||||||
|
|
||||||
|
// Introspect handles the OAuth 2.0 Token Introspection endpoint.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7662
|
||||||
|
Introspect(context.Context, *Request[oidc.IntrospectionRequest]) (*Response[oidc.IntrospectionResponse], error)
|
||||||
|
|
||||||
|
// UserInfo handles the UserInfo endpoint and returns Claims about the authenticated End-User.
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||||
|
UserInfo(context.Context, *Request[oidc.UserInfoRequest]) (*Response[oidc.UserInfo], error)
|
||||||
|
|
||||||
|
// Revocation handles token revocation using an access or refresh token.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7009
|
||||||
|
Revocation(context.Context, *Request[oidc.RevocationRequest]) (*Response[struct{}], error)
|
||||||
|
|
||||||
|
// EndSession handles the OpenID Connect RP-Initiated Logout.
|
||||||
|
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html
|
||||||
|
EndSession(context.Context, *Request[oidc.EndSessionRequest]) (*Response[struct{}], error)
|
||||||
|
|
||||||
|
// Keys serves the JWK set which the client can use verify signatures from the op.
|
||||||
|
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata `jwks_uri` key.
|
||||||
|
Keys(context.Context, *Request[struct{}]) (*Response[jose.JSONWebKeySet], error)
|
||||||
|
|
||||||
|
mustImpl()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnimplementedServer struct{}
|
||||||
|
|
||||||
|
// UnimplementedStatusCode is the statuscode returned for methods
|
||||||
|
// that are not yet implemented.
|
||||||
|
// Note that this means methods in the sense of the Go interface,
|
||||||
|
// and not http methods covered by "501 Not Implemented".
|
||||||
|
var UnimplementedStatusCode = http.StatusNotFound
|
||||||
|
|
||||||
|
func unimplementedError[T any](r *Request[T]) StatusError {
|
||||||
|
err := oidc.ErrServerError().WithDescription(fmt.Sprintf("%s not implemented on this server", r.URL.Path))
|
||||||
|
return StatusError{
|
||||||
|
parent: err,
|
||||||
|
statusCode: UnimplementedStatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) mustImpl() {}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Health(_ context.Context, r *Request[struct{}]) (*Response[Status], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Ready(_ context.Context, r *Request[struct{}]) (*Response[Status], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Discovery(_ context.Context, r *Request[struct{}]) (*Response[oidc.DiscoveryConfiguration], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Authorize(_ context.Context, r *Request[oidc.AuthRequest]) (*Response[url.URL], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) DeviceAuthorization(_ context.Context, r *Request[oidc.DeviceAuthorizationRequest]) (*Response[oidc.DeviceAuthorizationResponse], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) VerifyClient(_ context.Context, r *Request[ClientCredentials]) (Client, error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) CodeExchange(_ context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) RefreshToken(_ context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) JWTProfile(_ context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) TokenExchange(_ context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) ClientCredentialsExchange(_ context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) DeviceToken(_ context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
return nil, unimplementedError(r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Introspect(_ context.Context, r *Request[oidc.IntrospectionRequest]) (*Response[oidc.IntrospectionResponse], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) UserInfo(_ context.Context, r *Request[oidc.UserInfoRequest]) (*Response[oidc.UserInfo], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Revocation(_ context.Context, r *Request[oidc.RevocationRequest]) (*Response[struct{}], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) EndSession(_ context.Context, r *Request[oidc.EndSessionRequest]) (*Response[struct{}], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedServer) Keys(_ context.Context, r *Request[struct{}]) (*Response[jose.JSONWebKeySet], error) {
|
||||||
|
return nil, unimplementedError(r)
|
||||||
|
}
|
109
pkg/op/server_http.go
Normal file
109
pkg/op/server_http.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/rs/cors"
|
||||||
|
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webServer struct {
|
||||||
|
http.Handler
|
||||||
|
decoder httphelper.Decoder
|
||||||
|
server Server
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webServer) createRouter(endpoints *Endpoints, interceptors ...func(http.Handler) http.Handler) chi.Router {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Use(cors.New(defaultCORSOptions).Handler)
|
||||||
|
router.Use(interceptors...)
|
||||||
|
router.HandleFunc(healthEndpoint, healthHandler)
|
||||||
|
//router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
|
||||||
|
//router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage()))
|
||||||
|
//router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o))
|
||||||
|
//router.HandleFunc(authCallbackPath(o), authorizeCallbackHandler(o))
|
||||||
|
router.HandleFunc(endpoints.Token.Relative(), s.handleToken)
|
||||||
|
//router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
|
||||||
|
//router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
||||||
|
//router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o))
|
||||||
|
//router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o))
|
||||||
|
//router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
|
||||||
|
//router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), DeviceAuthorizationHandler(o))
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webServer) verifyRequestClient(r *http.Request) (Client, error) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return nil, oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err)
|
||||||
|
}
|
||||||
|
clientCredentials := new(ClientCredentials)
|
||||||
|
if err := s.decoder.Decode(clientCredentials, r.Form); err != nil {
|
||||||
|
return nil, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err)
|
||||||
|
}
|
||||||
|
// Basic auth takes precedence, so if set it overwrites the form data.
|
||||||
|
if clientID, clientSecret, ok := r.BasicAuth(); ok {
|
||||||
|
clientCredentials.ClientID, clientCredentials.ClientSecret = clientID, clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.server.VerifyClient(r.Context(), &Request[ClientCredentials]{
|
||||||
|
Method: r.Method,
|
||||||
|
URL: r.URL,
|
||||||
|
Header: r.Header,
|
||||||
|
Form: r.Form,
|
||||||
|
Data: clientCredentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webServer) handleToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
client, err := s.verifyRequestClient(r)
|
||||||
|
if err != nil {
|
||||||
|
RequestError(w, r, err, slog.Default())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grantType := oidc.GrantType(r.Form.Get("grant_type"))
|
||||||
|
var handle func(w http.ResponseWriter, r *http.Request, client Client)
|
||||||
|
switch grantType {
|
||||||
|
case oidc.GrantTypeCode:
|
||||||
|
handle = s.handleCodeExchange
|
||||||
|
case oidc.GrantTypeRefreshToken:
|
||||||
|
handle = s.handleRefreshToken
|
||||||
|
case "":
|
||||||
|
RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing"), slog.Default())
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
RequestError(w, r, oidc.ErrUnsupportedGrantType().WithDescription("%s not supported", grantType), slog.Default())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(w, r, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webServer) handleCodeExchange(w http.ResponseWriter, r *http.Request, client Client) {
|
||||||
|
request, err := decodeRequest[*oidc.AccessTokenRequest](s.decoder, r.Form)
|
||||||
|
if err != nil {
|
||||||
|
RequestError(w, r, err, s.logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := s.server.CodeExchange(r.Context(), newClientRequest(r, request, client))
|
||||||
|
if err != nil {
|
||||||
|
RequestError(w, r, err, s.logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.writeOut(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webServer) handleRefreshToken(w http.ResponseWriter, r *http.Request, client Client) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRequest[R any](decoder httphelper.Decoder, form map[string][]string) (request R, err error) {
|
||||||
|
if err := decoder.Decode(&request, form); err != nil {
|
||||||
|
return request, oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err)
|
||||||
|
}
|
||||||
|
return request, nil
|
||||||
|
}
|
81
pkg/op/server_legacy.go
Normal file
81
pkg/op/server_legacy.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LegacyServer struct {
|
||||||
|
UnimplementedServer
|
||||||
|
op *Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LegacyServer) VerifyClient(ctx context.Context, r *Request[ClientCredentials]) (Client, error) {
|
||||||
|
if r.Data.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
|
||||||
|
if !s.op.AuthMethodPrivateKeyJWTSupported() {
|
||||||
|
return nil, oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported")
|
||||||
|
}
|
||||||
|
return AuthorizePrivateJWTKey(ctx, r.Data.ClientAssertion, s.op)
|
||||||
|
}
|
||||||
|
client, err := s.op.Storage().GetClientByClientID(ctx, r.Data.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oidc.ErrInvalidClient().WithParent(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch client.AuthMethod() {
|
||||||
|
case oidc.AuthMethodNone:
|
||||||
|
return client, nil
|
||||||
|
case oidc.AuthMethodPrivateKeyJWT:
|
||||||
|
return nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client")
|
||||||
|
case oidc.AuthMethodPost:
|
||||||
|
if !s.op.AuthMethodPostSupported() {
|
||||||
|
return nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = AuthorizeClientIDSecret(ctx, r.Data.ClientID, r.Data.ClientSecret, s.op.storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LegacyServer) CodeExchange(ctx context.Context, r *ClientRequest[oidc.AccessTokenRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
authReq, err := AuthRequestByCode(ctx, s.op.storage, r.Data.Code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.Client.AuthMethod() == oidc.AuthMethodNone {
|
||||||
|
if err = AuthorizeCodeChallenge(r.Data.CodeVerifier, authReq.GetCodeChallenge()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := CreateTokenResponse(ctx, authReq, r.Client, s.op, true, r.Data.Code, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LegacyServer) RefreshToken(ctx context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response[oidc.AccessTokenResponse], error) {
|
||||||
|
if !ValidateGrantType(r.Client, oidc.GrantTypeRefreshToken) {
|
||||||
|
return nil, oidc.ErrUnauthorizedClient()
|
||||||
|
}
|
||||||
|
request, err := RefreshTokenRequestByRefreshToken(ctx, s.op.storage, r.Data.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.Client.GetID() != request.GetClientID() {
|
||||||
|
return nil, oidc.ErrInvalidGrant()
|
||||||
|
}
|
||||||
|
if err = ValidateRefreshTokenScopes(r.Data.Scopes, request); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := CreateTokenResponse(ctx, request, r.Client, s.op, true, "", r.Data.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewResponse(resp), nil
|
||||||
|
}
|
5
pkg/op/server_test.go
Normal file
5
pkg/op/server_test.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
// implementation check
|
||||||
|
var _ Server = &UnimplementedServer{}
|
||||||
|
var _ Server = &LegacyServer{}
|
|
@ -88,7 +88,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
err = AuthorizeCodeChallenge(tokenReq, request.GetCodeChallenge())
|
err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, request.GetCodeChallenge())
|
||||||
return request, client, err
|
return request, client, err
|
||||||
}
|
}
|
||||||
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
||||||
|
|
|
@ -117,11 +117,11 @@ func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string,
|
||||||
|
|
||||||
// AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent
|
// AuthorizeCodeChallenge authorizes a client by validating the code_verifier against the previously sent
|
||||||
// code_challenge of the auth request (PKCE)
|
// code_challenge of the auth request (PKCE)
|
||||||
func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error {
|
func AuthorizeCodeChallenge(codeVerifier string, challenge *oidc.CodeChallenge) error {
|
||||||
if tokenReq.CodeVerifier == "" {
|
if codeVerifier == "" {
|
||||||
return oidc.ErrInvalidRequest().WithDescription("code_challenge required")
|
return oidc.ErrInvalidRequest().WithDescription("code_challenge required")
|
||||||
}
|
}
|
||||||
if !oidc.VerifyCodeChallenge(challenge, tokenReq.CodeVerifier) {
|
if !oidc.VerifyCodeChallenge(challenge, codeVerifier) {
|
||||||
return oidc.ErrInvalidGrant().WithDescription("invalid code challenge")
|
return oidc.ErrInvalidGrant().WithDescription("invalid code challenge")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue