feat(OP): add back channel logout support (#671)
* feat: add configuration support for back channel logout * logout token * indicate back channel logout support in discovery endpoint
This commit is contained in:
parent
24869d2811
commit
f1e4cb2245
8 changed files with 151 additions and 23 deletions
28
README.md
28
README.md
|
@ -102,19 +102,20 @@ Here is json equivalent for one of the default users
|
|||
|
||||
## Features
|
||||
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
| -------------------- | ------------- | --------------- | ----------------------------------------- |
|
||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||
| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] |
|
||||
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
||||
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
||||
| JWT Profile | yes | yes | [RFC 7523][7] |
|
||||
| PKCE | yes | yes | [RFC 7636][8] |
|
||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||
| | Relying party | OpenID Provider | Specification |
|
||||
|----------------------| ------------- | --------------- |----------------------------------------------|
|
||||
| Code Flow | yes | yes | OpenID Connect Core 1.0, [Section 3.1][1] |
|
||||
| Implicit Flow | no[^1] | yes | OpenID Connect Core 1.0, [Section 3.2][2] |
|
||||
| Hybrid Flow | no | not yet | OpenID Connect Core 1.0, [Section 3.3][3] |
|
||||
| Client Credentials | yes | yes | OpenID Connect Core 1.0, [Section 9][4] |
|
||||
| Refresh Token | yes | yes | OpenID Connect Core 1.0, [Section 12][5] |
|
||||
| Discovery | yes | yes | OpenID Connect [Discovery][6] 1.0 |
|
||||
| JWT Profile | yes | yes | [RFC 7523][7] |
|
||||
| PKCE | yes | yes | [RFC 7636][8] |
|
||||
| Token Exchange | yes | yes | [RFC 8693][9] |
|
||||
| Device Authorization | yes | yes | [RFC 8628][10] |
|
||||
| mTLS | not yet | not yet | [RFC 8705][11] |
|
||||
| Back-Channel Logout | not yet | yes | OpenID Connect [Back-Channel Logout][12] 1.0 |
|
||||
|
||||
[1]: <https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth> "3.1. Authentication using the Authorization Code Flow"
|
||||
[2]: <https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth> "3.2. Authentication using the Implicit Flow"
|
||||
|
@ -127,6 +128,7 @@ Here is json equivalent for one of the default users
|
|||
[9]: <https://www.rfc-editor.org/rfc/rfc8693.html> "OAuth 2.0 Token Exchange"
|
||||
[10]: <https://www.rfc-editor.org/rfc/rfc8628.html> "OAuth 2.0 Device Authorization Grant"
|
||||
[11]: <https://www.rfc-editor.org/rfc/rfc8705.html> "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"
|
||||
[12]: <https://openid.net/specs/openid-connect-backchannel-1_0.html> "OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1"
|
||||
|
||||
## Contributors
|
||||
|
||||
|
|
|
@ -145,6 +145,14 @@ type DiscoveryConfiguration struct {
|
|||
|
||||
// OPTermsOfServiceURI is a URL the OpenID Provider provides to the person registering the Client to read about OpenID Provider's terms of service.
|
||||
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
|
||||
|
||||
// BackChannelLogoutSupported specifies whether the OP supports back-channel logout (https://openid.net/specs/openid-connect-backchannel-1_0.html),
|
||||
// with true indicating support. If omitted, the default value is false.
|
||||
BackChannelLogoutSupported bool `json:"backchannel_logout_supported,omitempty"`
|
||||
|
||||
// BackChannelLogoutSessionSupported specifies whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP session with the OP.
|
||||
// If supported, the sid Claim is also included in ID Tokens issued by the OP. If omitted, the default value is false.
|
||||
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported,omitempty"`
|
||||
}
|
||||
|
||||
type AuthMethod string
|
||||
|
|
|
@ -382,3 +382,40 @@ type TokenExchangeResponse struct {
|
|||
// if the requested_token_type was Access Token and scope contained openid.
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
type LogoutTokenClaims struct {
|
||||
Issuer string `json:"iss,omitempty"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Audience Audience `json:"aud,omitempty"`
|
||||
IssuedAt Time `json:"iat,omitempty"`
|
||||
Expiration Time `json:"exp,omitempty"`
|
||||
JWTID string `json:"jti,omitempty"`
|
||||
Events map[string]any `json:"events,omitempty"`
|
||||
SessionID string `json:"sid,omitempty"`
|
||||
Claims map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
type ltcAlias LogoutTokenClaims
|
||||
|
||||
func (i *LogoutTokenClaims) MarshalJSON() ([]byte, error) {
|
||||
return mergeAndMarshalClaims((*ltcAlias)(i), i.Claims)
|
||||
}
|
||||
|
||||
func (i *LogoutTokenClaims) UnmarshalJSON(data []byte) error {
|
||||
return unmarshalJSONMulti(data, (*ltcAlias)(i), &i.Claims)
|
||||
}
|
||||
|
||||
func NewLogoutTokenClaims(issuer, subject string, audience Audience, expiration time.Time, jwtID, sessionID string, skew time.Duration) *LogoutTokenClaims {
|
||||
return &LogoutTokenClaims{
|
||||
Issuer: issuer,
|
||||
Subject: subject,
|
||||
Audience: audience,
|
||||
IssuedAt: FromTime(time.Now().Add(-skew)),
|
||||
Expiration: FromTime(expiration),
|
||||
JWTID: jwtID,
|
||||
Events: map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
SessionID: sessionID,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,3 +242,39 @@ func TestIDTokenClaims_GetUserInfo(t *testing.T) {
|
|||
got := idTokenData.GetUserInfo()
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestNewLogoutTokenClaims(t *testing.T) {
|
||||
want := &LogoutTokenClaims{
|
||||
Issuer: "zitadel",
|
||||
Subject: "hello@me.com",
|
||||
Audience: Audience{"foo", "just@me.com"},
|
||||
Expiration: 12345,
|
||||
JWTID: "jwtID",
|
||||
Events: map[string]any{
|
||||
"http://schemas.openid.net/event/backchannel-logout": struct{}{},
|
||||
},
|
||||
SessionID: "sessionID",
|
||||
Claims: nil,
|
||||
}
|
||||
|
||||
got := NewLogoutTokenClaims(
|
||||
want.Issuer,
|
||||
want.Subject,
|
||||
want.Audience,
|
||||
want.Expiration.AsTime(),
|
||||
want.JWTID,
|
||||
want.SessionID,
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
// test if the dynamic timestamp is around now,
|
||||
// allowing for a delta of 1, just in case we flip on
|
||||
// either side of a second boundry.
|
||||
nowMinusSkew := NowTime() - 1
|
||||
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||
|
||||
// Make equal not fail on dynamic timestamp
|
||||
got.IssuedAt = 0
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
|
|
@ -49,6 +49,9 @@ type Configuration interface {
|
|||
|
||||
SupportedUILocales() []language.Tag
|
||||
DeviceAuthorization() DeviceAuthorizationConfig
|
||||
|
||||
BackChannelLogoutSupported() bool
|
||||
BackChannelLogoutSessionSupported() bool
|
||||
}
|
||||
|
||||
type IssuerFromRequest func(r *http.Request) string
|
||||
|
|
|
@ -61,6 +61,8 @@ func CreateDiscoveryConfig(ctx context.Context, config Configuration, storage Di
|
|||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||
UILocalesSupported: config.SupportedUILocales(),
|
||||
RequestParameterSupported: config.RequestObjectSupported(),
|
||||
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +94,8 @@ func createDiscoveryConfigV2(ctx context.Context, config Configuration, storage
|
|||
CodeChallengeMethodsSupported: CodeChallengeMethods(config),
|
||||
UILocalesSupported: config.SupportedUILocales(),
|
||||
RequestParameterSupported: config.RequestObjectSupported(),
|
||||
BackChannelLogoutSupported: config.BackChannelLogoutSupported(),
|
||||
BackChannelLogoutSessionSupported: config.BackChannelLogoutSessionSupported(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,34 @@ func (mr *MockConfigurationMockRecorder) AuthorizationEndpoint() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint))
|
||||
}
|
||||
|
||||
// BackChannelLogoutSessionSupported mocks base method.
|
||||
func (m *MockConfiguration) BackChannelLogoutSessionSupported() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BackChannelLogoutSessionSupported")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BackChannelLogoutSessionSupported indicates an expected call of BackChannelLogoutSessionSupported.
|
||||
func (mr *MockConfigurationMockRecorder) BackChannelLogoutSessionSupported() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSessionSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSessionSupported))
|
||||
}
|
||||
|
||||
// BackChannelLogoutSupported mocks base method.
|
||||
func (m *MockConfiguration) BackChannelLogoutSupported() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BackChannelLogoutSupported")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BackChannelLogoutSupported indicates an expected call of BackChannelLogoutSupported.
|
||||
func (mr *MockConfigurationMockRecorder) BackChannelLogoutSupported() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackChannelLogoutSupported", reflect.TypeOf((*MockConfiguration)(nil).BackChannelLogoutSupported))
|
||||
}
|
||||
|
||||
// CodeMethodS256Supported mocks base method.
|
||||
func (m *MockConfiguration) CodeMethodS256Supported() bool {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
30
pkg/op/op.go
30
pkg/op/op.go
|
@ -158,16 +158,18 @@ func authCallbackPath(o OpenIDProvider) string {
|
|||
}
|
||||
|
||||
type Config struct {
|
||||
CryptoKey [32]byte
|
||||
DefaultLogoutRedirectURI string
|
||||
CodeMethodS256 bool
|
||||
AuthMethodPost bool
|
||||
AuthMethodPrivateKeyJWT bool
|
||||
GrantTypeRefreshToken bool
|
||||
RequestObjectSupported bool
|
||||
SupportedUILocales []language.Tag
|
||||
SupportedClaims []string
|
||||
DeviceAuthorization DeviceAuthorizationConfig
|
||||
CryptoKey [32]byte
|
||||
DefaultLogoutRedirectURI string
|
||||
CodeMethodS256 bool
|
||||
AuthMethodPost bool
|
||||
AuthMethodPrivateKeyJWT bool
|
||||
GrantTypeRefreshToken bool
|
||||
RequestObjectSupported bool
|
||||
SupportedUILocales []language.Tag
|
||||
SupportedClaims []string
|
||||
DeviceAuthorization DeviceAuthorizationConfig
|
||||
BackChannelLogoutSupported bool
|
||||
BackChannelLogoutSessionSupported bool
|
||||
}
|
||||
|
||||
// Endpoints defines endpoint routes.
|
||||
|
@ -411,6 +413,14 @@ func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
|
|||
return o.config.DeviceAuthorization
|
||||
}
|
||||
|
||||
func (o *Provider) BackChannelLogoutSupported() bool {
|
||||
return o.config.BackChannelLogoutSupported
|
||||
}
|
||||
|
||||
func (o *Provider) BackChannelLogoutSessionSupported() bool {
|
||||
return o.config.BackChannelLogoutSessionSupported
|
||||
}
|
||||
|
||||
func (o *Provider) Storage() Storage {
|
||||
return o.storage
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue