Skip to content

Commit

Permalink
add multi-login logout redirect support
Browse files Browse the repository at this point in the history
  • Loading branch information
bastianccm committed Jan 27, 2020
1 parent 85591ce commit c655895
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 31 deletions.
35 changes: 28 additions & 7 deletions core/auth/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ package auth
import (
"context"
"errors"
"net/url"

"flamingo.me/flamingo/v3/framework/web"
)

// controller manages login and callback requests
type controller struct {
service *WebIdentityService
responder *web.Responder
service *WebIdentityService
responder *web.Responder
reverseRouter web.ReverseRouter
}

// Inject WebIdentityService dependency
func (c *controller) Inject(service *WebIdentityService, responder *web.Responder) {
func (c *controller) Inject(service *WebIdentityService, responder *web.Responder, reverseRouter web.ReverseRouter) {
c.service = service
c.responder = responder
c.reverseRouter = reverseRouter
}

// Callback is called e.g. for OIDC
Expand All @@ -43,12 +46,30 @@ func (c *controller) Login(ctx context.Context, request *web.Request) web.Result

// LogoutAll removes all identities
func (c *controller) LogoutAll(ctx context.Context, request *web.Request) web.Result {
c.service.Logout(ctx, request)
return c.responder.RouteRedirect("", nil)
return c.service.Logout(ctx, request, nil)
}

// Logout removes one identity
func (c *controller) Logout(ctx context.Context, request *web.Request) web.Result {
c.service.LogoutFor(ctx, request.Params["broker"], request)
return c.responder.RouteRedirect("", nil)
return c.service.LogoutFor(ctx, request.Params["broker"], request, nil)
}

// LogoutCallback redirects to the next upcoming redirect url
func (c *controller) LogoutCallback(ctx context.Context, request *web.Request) web.Result {
redirects := c.service.getLogoutRedirects(request)
if len(redirects) == 0 {
if postRedirect, ok := request.Session().Load("core.auth.logoutredirect"); ok {
request.Session().Delete("core.auth.logoutredirect")
return c.responder.URLRedirect(postRedirect.(*url.URL))
}
return c.responder.RouteRedirect("", nil)
}
next := redirects[0]
c.service.storeLogoutRedirects(request, redirects[1:])
if postRedirect, err := c.reverseRouter.Absolute(request, "core.auth.logoutCallback", nil); err == nil {
query := next.Query()
query.Set("post_logout_redirect_uri", postRedirect.String())
next.RawQuery = query.Encode()
}
return c.responder.URLRedirect(next)
}
6 changes: 2 additions & 4 deletions core/auth/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,10 @@ func (c *debugController) Action(ctx context.Context, request *web.Request) web.
}
return c.identityService.AuthenticateFor(ctx, broker, request)
case "logoutall":
c.identityService.Logout(ctx, request)
return c.responder.URLRedirect(&url.URL{ForceQuery: true})
return c.identityService.Logout(ctx, request, &url.URL{Path: request.Request().URL.Path, ForceQuery: true})
case "logout":
broker, _ := request.Query1("__debug__broker")
c.identityService.LogoutFor(ctx, broker, request)
return c.responder.URLRedirect(&url.URL{ForceQuery: true})
return c.identityService.LogoutFor(ctx, broker, request, &url.URL{Path: request.Request().URL.Path, ForceQuery: true})
}

buf := new(bytes.Buffer)
Expand Down
2 changes: 2 additions & 0 deletions core/auth/example/config/config.cue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ core: auth: {
core.auth.http & {broker: "http1", realm: "http1 realm", users: {"user1": "pw1"}},
core.auth.oidc & {broker: "oidc2", clientID: "client2", clientSecret: "client2", "endpoint": "http://127.0.0.1:3352/dex"},
core.auth.http & {broker: "http2", realm: "http2 realm", users: {"user2": "pw2"}},
core.auth.oidc & {broker: "kc1", clientID: "client1", clientSecret: "", "endpoint": "http://127.0.0.1:3353/auth/realms/Realm1", enableOfflineToken: false},
core.auth.oidc & {broker: "kc2", clientID: "client2", clientSecret: "", "endpoint": "http://127.0.0.1:3354/auth/realms/Realm2", enableOfflineToken: false},
]
}

Expand Down
22 changes: 22 additions & 0 deletions core/auth/example/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,25 @@ services:
command: serve /dex.yml
ports:
- 3352:3352
keycloak1:
image: quay.io/keycloak/keycloak:8.0.1
environment:
DB_VENDOR: h2
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: /tmp/realm.json
volumes:
- ./realm1.json:/tmp/realm.json
ports:
- 3353:8080
keycloak2:
image: quay.io/keycloak/keycloak:8.0.1
environment:
DB_VENDOR: h2
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: /tmp/realm.json
volumes:
- ./realm2.json:/tmp/realm.json
ports:
- 3354:8080
92 changes: 92 additions & 0 deletions core/auth/example/realm1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"realm": "Realm1",
"displayName": "Realm 1",
"displayNameHtml": "Realm 1",
"revokeRefreshToken": true,
"refreshTokenMaxReuse": 1,
"accessTokenLifespan": 20,
"ssoSessionMaxLifespan": 18000,
"offlineSessionIdleTimeout": 86400,
"accessCodeLifespanLogin": 180,
"enabled": true,
"sslRequired": "none",
"bruteForceProtected": true,
"loginTheme": "keycloak",
"accountTheme": "keycloak",
"adminTheme": "keycloak",
"emailTheme": "keycloak",
"internationalizationEnabled": true,
"supportedLocales": [
"en"
],
"defaultLocale": "en",
"passwordPolicy": "lowerCase(1)",
"clients": [
{
"clientId": "client1",
"name": "client1",
"description": "client1",
"redirectUris": [
"*"
],
"webOrigins": [],
"publicClient": true,
"surrogateAuthRequired": false,
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "client1",
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"frontchannelLogout": true,
"protocol": "openid-connect",
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": true,
"consentText": "${email}",
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
}
],
"useTemplateConfig": false,
"useTemplateScope": false,
"useTemplateMappers": false,
"access": {
"view": true,
"configure": true,
"manage": true
}
}
],
"users": [
{
"enabled" : true,
"emailVerified" : true,
"username": "user",
"firstName" : "First",
"lastName" : "Last",
"email": "user@example.com",
"credentials": [
{
"type" : "password",
"value" : "password"
}
]
}
]
}
92 changes: 92 additions & 0 deletions core/auth/example/realm2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"realm": "Realm2",
"displayName": "Realm 2",
"displayNameHtml": "Realm 2",
"revokeRefreshToken": true,
"refreshTokenMaxReuse": 1,
"accessTokenLifespan": 20,
"ssoSessionMaxLifespan": 18000,
"offlineSessionIdleTimeout": 86400,
"accessCodeLifespanLogin": 180,
"enabled": true,
"sslRequired": "none",
"bruteForceProtected": true,
"loginTheme": "keycloak",
"accountTheme": "keycloak",
"adminTheme": "keycloak",
"emailTheme": "keycloak",
"internationalizationEnabled": true,
"supportedLocales": [
"en"
],
"defaultLocale": "en",
"passwordPolicy": "lowerCase(1)",
"clients": [
{
"clientId": "client2",
"name": "client2",
"description": "client2",
"redirectUris": [
"*"
],
"webOrigins": [],
"publicClient": true,
"surrogateAuthRequired": false,
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "client2",
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"frontchannelLogout": true,
"protocol": "openid-connect",
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": true,
"consentText": "${email}",
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
}
],
"useTemplateConfig": false,
"useTemplateScope": false,
"useTemplateMappers": false,
"access": {
"view": true,
"configure": true,
"manage": true
}
}
],
"users": [
{
"enabled" : true,
"emailVerified" : true,
"username": "user",
"firstName" : "First",
"lastName" : "Last",
"email": "user@example.com",
"credentials": [
{
"type" : "password",
"value" : "password"
}
]
}
]
}
2 changes: 2 additions & 0 deletions core/auth/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ func (r *routes) Routes(router *web.RouterRegistry) {
router.HandleAny("core.auth.logoutall", r.controller.LogoutAll)
_, _ = router.Route("/core/auth/logout/:broker", "core.auth.logout(broker)")
router.HandleAny("core.auth.logout", r.controller.Logout)
_, _ = router.Route("/core/auth/logoutCallback", "core.auth.logoutCallback")
router.HandleAny("core.auth.logoutCallback", r.controller.LogoutCallback)
}

// CueConfig schema
Expand Down
1 change: 1 addition & 0 deletions core/auth/oauth/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ core: auth: {
idToken: [...string]
userInfo: [...string]
}
enableEndSessionEndpoint: bool | *true
}
}
`
Expand Down
44 changes: 35 additions & 9 deletions core/auth/oauth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,21 @@ type (
}

oidcConfig struct {
Broker string `json:"broker"`
Endpoint string `json:"endpoint"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
Scopes []string `json:"scopes"`
EnabledOfflineToken bool `json:"enabledOfflineToken"`
Claimset struct {
Broker string `json:"broker"`
Endpoint string `json:"endpoint"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
Scopes []string `json:"scopes"`
EnableOfflineToken bool `json:"enableOfflineToken"`
Claimset struct {
IDToken []string `json:"idToken"`
UserInfo []string `json:"userInfo"`
} `json:"requestClaims"`
Claims struct {
IDToken map[string]string `json:"idToken"`
AccessToken map[string]string `json:"accessToken"`
} `json:"claims"`
EnableEndSessionEndpoint bool `json:"enableEndSessionEndpoint"`
}
)

Expand All @@ -93,7 +94,7 @@ func oidcFactory(cfg config.Map) (auth.RequestIdentifier, error) {
}

scopes := append([]string{oidc.ScopeOpenID}, oidcConfig.Scopes...)
if oidcConfig.EnabledOfflineToken {
if oidcConfig.EnableOfflineToken {
scopes = append(scopes, oidc.ScopeOfflineAccess)
}

Expand Down Expand Up @@ -367,8 +368,33 @@ func (i *openIDIdentifier) Callback(ctx context.Context, request *web.Request, r
}

// Logout based on a request
func (i *openIDIdentifier) Logout(ctx context.Context, request *web.Request) {
func (i *openIDIdentifier) Logout(ctx context.Context, request *web.Request) *url.URL {
identity, err := i.Identify(ctx, request)
request.Session().Delete(i.sessionCode("sessiondata"))

// return if we are not logged in
if identity == nil || err != nil || !i.oidcConfig.EnableEndSessionEndpoint {
return nil
}

var claims struct {
EndSessionEndpoint string `json:"end_session_endpoint"`
}
// we can ignore errors here as we are just fine handling default values
if err := i.OpenIDConnectProvider().Claims(&claims); err != nil {
return nil
}
if claims.EndSessionEndpoint == "" {
return nil
}
returnURL, err := url.Parse(claims.EndSessionEndpoint)
if err != nil {
return nil
}
query := returnURL.Query()
query.Set("id_token_hint", identity.(*oidcIdentity).rawIDToken)
returnURL.RawQuery = query.Encode()
return returnURL
}

// OpenIDConnectProvider getter for openID Connect Provider
Expand Down
Loading

0 comments on commit c655895

Please sign in to comment.