Skip to content

Commit

Permalink
feat(OP): add back channel logout support (#671)
Browse files Browse the repository at this point in the history
* feat: add configuration support for back channel logout

* logout token

* indicate back channel logout support in discovery endpoint
  • Loading branch information
livio-a authored Oct 30, 2024
1 parent 24869d2 commit f1e4cb2
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 23 deletions.
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions pkg/oidc/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions pkg/oidc/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
36 changes: 36 additions & 0 deletions pkg/oidc/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]",
Audience: Audience{"foo", "[email protected]"},
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)
}
3 changes: 3 additions & 0 deletions pkg/op/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type Configuration interface {

SupportedUILocales() []language.Tag
DeviceAuthorization() DeviceAuthorizationConfig

BackChannelLogoutSupported() bool
BackChannelLogoutSessionSupported() bool
}

type IssuerFromRequest func(r *http.Request) string
Expand Down
4 changes: 4 additions & 0 deletions pkg/op/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -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(),
}
}

Expand Down
28 changes: 28 additions & 0 deletions pkg/op/mock/configuration.mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 20 additions & 10 deletions pkg/op/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit f1e4cb2

Please sign in to comment.