This issue proposes a new interface Server in the op package. It's purpose is to obsolete the current Storage interface and Provider type over time.
Motivation
Community feedback
From the community (and at zitadel we feel the same pain):
Maintainability
In the current design there is a mix of business logic and the implementation of the OpenID connect / Oauth2 standards. Over time this resulted in overly complex code and duplication of code which is hard to understand and maintain.
Some examples
|
// AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered. |
|
// It than returns the auth request corresponding to the auth code |
|
func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (request AuthRequest, client Client, err error) { |
|
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { |
|
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) |
|
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { |
|
return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") |
|
} |
|
client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) |
|
return request, client, err |
|
} |
|
client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) |
|
if err != nil { |
|
return nil, nil, oidc.ErrInvalidClient().WithParent(err) |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { |
|
return nil, nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client") |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodNone { |
|
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
err = AuthorizeCodeChallenge(tokenReq, request.GetCodeChallenge()) |
|
return request, client, err |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { |
|
return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") |
|
} |
|
err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code) |
|
return request, client, err |
|
} |
|
// AuthorizeRefreshClient checks the authorization of the client and that the used method was the one previously registered. |
|
// It than returns the data representing the original auth request corresponding to the refresh_token |
|
func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) { |
|
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { |
|
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) |
|
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { |
|
return nil, nil, errors.New("auth_method private_key_jwt not supported") |
|
} |
|
client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { |
|
return nil, nil, oidc.ErrUnauthorizedClient() |
|
} |
|
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) |
|
return request, client, err |
|
} |
|
client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
if !ValidateGrantType(client, oidc.GrantTypeRefreshToken) { |
|
return nil, nil, oidc.ErrUnauthorizedClient() |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { |
|
return nil, nil, oidc.ErrInvalidClient() |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodNone { |
|
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) |
|
return request, client, err |
|
} |
|
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { |
|
return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported") |
|
} |
|
if err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()); err != nil { |
|
return nil, nil, err |
|
} |
|
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken) |
|
return request, client, err |
|
} |
Performance
Due to the business logic in OIDC and its reliance on the Storage interface. We found that there are multiple calls to certain methods. For example, we found for the token endpoint there are cases where GetUserByID is called 3 times. If we would sperate the business logic from implementation we could optimized this by a single call. Basically, business logic shouldn't be scope of the OIDC framework.
Proposed interface
It will be the scope of this framework to:
- Route requests to their methods;
- Parse request data into a type that represents the model from the standards;
- Validate required fields in the request;
- Provide utility functions that helps implementations build the response;
It will be the scope of the implementer
- Handle the request data to obtain or modify state in their storage / database;
- Implement the business logic that is needed to build the response;
// Server describes the interface that needs to be implemented to serve
// OpenID Connect and Oauth2 standard requests.
//
// Methods are called after the HTTP route is resolved and
// the request body is parsed into the Request's Data field.
// When a method is called, it can be assumed that required fields,
// as described in their relevant standard, are validated already.
// The Response Data field may be of any type to allow flexibility
// to extend responses with custom fields. There are however requirements
// in the standards regarding the response models. Where applicable
// the method documentation gives a recommended type which can be used
// directly or extended upon.
type Server interface {
// Health should return a status of "ok" once the Server is listining.
// The recommended Response Data type is [Status].
Health(context.Context, *Request[struct{}]) (*Response, 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.
// The recommended Response Data type is [Status].
Ready(context.Context, *Request[struct{}]) (*Response, error)
// Discovery return the OpenID Provider Configuration Information for this server.
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
// The recommended Response Data type is [oidc.DiscoveryConfiguration].
Discovery(context.Context, *Request[struct{}]) (*Response, 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]) (*Redirect, error)
// AuthorizeCallback? Do we still need it?
// DeviceAuthorization initiates the device authorization flow.
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
// The recommended Response Data type is [oidc.DeviceAuthorizationResponse].
DeviceAuthorization(context.Context, *Request[oidc.DeviceAuthorizationRequest]) (*Response, 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
// The recommended Response Data type is [oidc.AccessTokenResponse].
CodeExchange(context.Context, *ClientRequest[oidc.AccessTokenRequest]) (*Response, 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
// The recommended Response Data type is [oidc.AccessTokenResponse].
RefreshToken(context.Context, *ClientRequest[oidc.RefreshTokenRequest]) (*Response, 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
// The recommended Response Data type is [oidc.AccessTokenResponse].
JWTProfile(context.Context, *Request[oidc.JWTProfileGrantRequest]) (*Response, 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
// The recommended Response Data type is [oidc.AccessTokenResponse].
TokenExchange(context.Context, *ClientRequest[oidc.TokenExchangeRequest]) (*Response, 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
// The recommended Response Data type is [oidc.AccessTokenResponse].
ClientCredentialsExchange(context.Context, *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, 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.
// The recommended Response Data type is [oidc.AccessTokenResponse].
DeviceToken(context.Context, *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error)
// Introspect handles the OAuth 2.0 Token Introspection endpoint.
// https://datatracker.ietf.org/doc/html/rfc7662
// The recommended Response Data type is [oidc.IntrospectionResponse].
Introspect(context.Context, *Request[oidc.IntrospectionRequest]) (*Response, 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
// The recommended Response Data type is [oidc.UserInfo].
UserInfo(context.Context, *Request[oidc.UserInfoRequest]) (*Response, error)
// Revocation handles token revocation using an access or refresh token.
// https://datatracker.ietf.org/doc/html/rfc7009
// There are no response requirements. Data may remain empty.
Revocation(context.Context, *Request[oidc.RevocationRequest]) (*Response, error)
// EndSession handles the OpenID Connect RP-Initiated Logout.
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html
// There are no response requirements. Data may remain empty.
EndSession(context.Context, *Request[oidc.EndSessionRequest]) (*Response, 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.
// The recommended Response Data type is [jose.JSOMWebKeySet].
Keys(context.Context, *Request[struct{}]) (*Response, error)
// mustImpl forces implementations to embed the UnimplementedServer for forward
// compatibilty with the interface.
mustImpl()
}
// Request contains the [http.Request] informational fields
// and parsed Data from the request body (POST) or URL parameters (GET).
// Data can be assumed to be validated according to the applicable
// standard for the specific endpoints.
type Request[T any] struct {
Method string
URL *url.URL
Header http.Header
Form url.Values
PostForm url.Values
Data *T
}
// ClientRequest is a Request with a verified client attached to it.
// Methods the recieve this argument may assume the client was authenticated,
// or verified to be a public client.
type ClientRequest[T any] struct {
*Request[T]
Client Client
}
type Response struct {
// Header map will be merged with the
// header on the [http.ResponseWriter].
Header http.Header
// Data will be JSON marshaled to
// the response body.
// We allow any type, so that implementations
// can extend the standard types as they wish.
// However, each method will recommend which
// (base) type to use as model, in order to
// be complaint with the standards.
Data any
}
// Redirect is a special response type which will
// initiate a [http.StatusFound] redirect.
// The Params field will be encoded and set to the
// URL's RawQuery field before building the URL.
//
// If the RawQuery contains values that need to persist,
// the implementation should parse them into Params and
// add request specific values after.
type Redirect struct {
// Header map will be merged with the
// header on the [http.ResponseWriter].
Header http.Header
URL url.URL
Params url.Values
}
Questions
There are still some doubts in the above design I would like to clarify:
AuthorizeCallback is an endpoint which is not part of the standard, but required for Zitadel's login mechanism. Should it be part of this interface? (I believe it shouldn't, nut I'm open to other opinions)
- Allowing
any in the Response object, is that the right way to go? In my original concept I used the concrete types defined in the oidc package, but figured that I would prevent flexibility if implementations need to return more. I've played with the idea of providing an Extra map field for that purpose, but that would lead into messy code trying to merge and marshal the struct and map types. In the end json.Marshal takes an interface type anyway. On the flip side is that now it is not clear what response type is required from the method signature alone. One would always need to read the comments in the interface comments.
Alternative response definitions
type Response[T any] struct {
// Header map will be merged with the
// header on the [http.ResponseWriter].
Header http.Header
// Data will be JSON marshaled to
// the response body.
Data T
Extra map[string]any
}
...
CodeExchange(context.Context, *ClientRequest[oidc.AccessTokenRequest]) (*Response[*oidc.AccessTokenResponse], error)
...
Forward compatibility
What we've learned so far is that a central interface like Storage, needs to grows over time to suit different needs. We ended up with many optional interfaces and type assertions throughout the framework. Inspired much by how the gRPC ecosystem solves this,we want to provide and UnimplementedServer implementation. The UnimplementedServer will implement all methods but will return an error describing that the particular endpoint isn't implemented on that server.
Implementations will need to embed the UnimplementedServer in their struct type, so that we can keep adding methods without breaking those implementations. This is enforced by the mustImpl() un-exported method.
How we match that with the Endpoints configuration and discovery still needs to be determined.
Unimplemented server
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, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) Ready(_ context.Context, r *Request[struct{}]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) Discovery(_ context.Context, r *Request[struct{}]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) Authorize(_ context.Context, r *Request[oidc.AuthRequest]) (*Redirect, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) DeviceAuthorization(_ context.Context, r *Request[oidc.DeviceAuthorizationRequest]) (*Response, 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, error) {
return nil, unimplementedError(r.Request)
}
func (UnimplementedServer) RefreshToken(_ context.Context, r *ClientRequest[oidc.RefreshTokenRequest]) (*Response, error) {
return nil, unimplementedError(r.Request)
}
func (UnimplementedServer) JWTProfile(_ context.Context, r *Request[oidc.JWTProfileGrantRequest]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) TokenExchange(_ context.Context, r *ClientRequest[oidc.TokenExchangeRequest]) (*Response, error) {
return nil, unimplementedError(r.Request)
}
func (UnimplementedServer) ClientCredentialsExchange(_ context.Context, r *ClientRequest[oidc.ClientCredentialsRequest]) (*Response, error) {
return nil, unimplementedError(r.Request)
}
func (UnimplementedServer) DeviceToken(_ context.Context, r *ClientRequest[oidc.DeviceAccessTokenRequest]) (*Response, error) {
return nil, unimplementedError(r.Request)
}
func (UnimplementedServer) Introspect(_ context.Context, r *Request[oidc.IntrospectionRequest]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) UserInfo(_ context.Context, r *Request[oidc.UserInfoRequest]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) Revocation(_ context.Context, r *Request[oidc.RevocationRequest]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) EndSession(_ context.Context, r *Request[oidc.EndSessionRequest]) (*Response, error) {
return nil, unimplementedError(r)
}
func (UnimplementedServer) Keys(_ context.Context, r *Request[struct{}]) (*Response, error) {
return nil, unimplementedError(r)
}
Backward compatibility
When we will release the new Server interface, we wil also bring an implementation that uses the current Storage and Provider underneath. That means implementations should be able to switch over without the direct need of refactoring their code base. There might be some small breaking changes in exported functions that are currently used inside the handlers, that's why we target v3.
There is already a small proof of concept for Code Exchange and Refresh Token that ties the new Server to the old Provider here:
Legacy Server
|
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, 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, 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 |
|
} |
Planning
We intend to implement an experimental version of this interface into the V3 release. Current users of the Provider and Storage interfaces will not directly be affected by the addition and have a choice to use the old Provider, Provider wrapped in a LegacyServer or start implementing from scratch. Following inclusion, we will have to start re-writing most functions that that Storage arguments to accept valued arguments instead. Once that is completed we will be able to start deprecating the Provider and Storage types.
General Roadmap:
- pre-v3 (breaking changes):
- Definition of
Server interface, state experimental.
LegacyServer implementation.
- post-v3 (feature additions):
- Adding configuration options based on current
Provider.
- pre-v4:
- Stabilization of the
Server interface
- Removal of direct
Storage calls in exported functions, use values instead.
- Deprecation of
Storage.
After Storage has been deprecated, we will stop accepting new features for it. Actual removal will depend on community needs.
CC: @muir @livio-a @adlerhurst
This issue proposes a new interface
Serverin theoppackage. It's purpose is to obsolete the current Storage interface and Provider type over time.Motivation
Community feedback
From the community (and at zitadel we feel the same pain):
Maintainability
In the current design there is a mix of business logic and the implementation of the OpenID connect / Oauth2 standards. Over time this resulted in overly complex code and duplication of code which is hard to understand and maintain.
Some examples
oidc/pkg/op/token_code.go
Lines 64 to 103 in daf82a5
oidc/pkg/op/token_refresh.go
Lines 89 to 129 in daf82a5
Performance
Due to the business logic in OIDC and its reliance on the Storage interface. We found that there are multiple calls to certain methods. For example, we found for the token endpoint there are cases where
GetUserByIDis called 3 times. If we would sperate the business logic from implementation we could optimized this by a single call. Basically, business logic shouldn't be scope of the OIDC framework.Proposed interface
It will be the scope of this framework to:
It will be the scope of the implementer
Questions
There are still some doubts in the above design I would like to clarify:
AuthorizeCallbackis an endpoint which is not part of the standard, but required for Zitadel's login mechanism. Should it be part of this interface? (I believe it shouldn't, nut I'm open to other opinions)anyin the Response object, is that the right way to go? In my original concept I used the concrete types defined in theoidcpackage, but figured that I would prevent flexibility if implementations need to return more. I've played with the idea of providing anExtramap field for that purpose, but that would lead into messy code trying to merge and marshal the struct and map types. In the endjson.Marshaltakes an interface type anyway. On the flip side is that now it is not clear what response type is required from the method signature alone. One would always need to read the comments in the interface comments.Alternative response definitions
Forward compatibility
What we've learned so far is that a central interface like
Storage, needs to grows over time to suit different needs. We ended up with many optional interfaces and type assertions throughout the framework. Inspired much by how the gRPC ecosystem solves this,we want to provide andUnimplementedServerimplementation. TheUnimplementedServerwill implement all methods but will return an error describing that the particular endpoint isn't implemented on that server.Implementations will need to embed the
UnimplementedServerin their struct type, so that we can keep adding methods without breaking those implementations. This is enforced by themustImpl()un-exported method.How we match that with the
Endpointsconfiguration and discovery still needs to be determined.Unimplemented server
Backward compatibility
When we will release the new
Serverinterface, we wil also bring an implementation that uses the currentStorageandProviderunderneath. That means implementations should be able to switch over without the direct need of refactoring their code base. There might be some small breaking changes in exported functions that are currently used inside the handlers, that's why we target v3.There is already a small proof of concept for Code Exchange and Refresh Token that ties the new
Serverto the oldProviderhere:Legacy Server
oidc/pkg/op/server_legacy.go
Lines 9 to 81 in c340ed9
Planning
We intend to implement an experimental version of this interface into the V3 release. Current users of the
ProviderandStorageinterfaces will not directly be affected by the addition and have a choice to use the oldProvider,Providerwrapped in aLegacyServeror start implementing from scratch. Following inclusion, we will have to start re-writing most functions that thatStoragearguments to accept valued arguments instead. Once that is completed we will be able to start deprecating theProviderandStoragetypes.General Roadmap:
Serverinterface, state experimental.LegacyServerimplementation.Provider.ServerinterfaceStoragecalls in exported functions, use values instead.Storage.After
Storagehas been deprecated, we will stop accepting new features for it. Actual removal will depend on community needs.CC: @muir @livio-a @adlerhurst