From b50557a5ee026d131ee60e59027fc4d50953ba34 Mon Sep 17 00:00:00 2001 From: Mark Phelps <209477+markphelps@users.noreply.github.com> Date: Tue, 14 May 2024 13:18:38 -0400 Subject: [PATCH] feat: logout redirect to cloud issuer (#3082) * feat: logout redirect to cloud issuer Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com> * chore: fix middleware test Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com> --------- Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- cmd/flipt/cloud.go | 2 +- .../authn/middleware/grpc/middleware.go | 4 + .../authn/middleware/grpc/middleware_test.go | 21 ++--- ui/src/components/Header.tsx | 4 +- ui/src/components/header/UserProfile.tsx | 78 ++++++++++--------- ui/src/types/auth/Github.ts | 1 + ui/src/types/auth/JWT.ts | 2 + ui/src/types/auth/OIDC.ts | 1 + 8 files changed, 61 insertions(+), 52 deletions(-) diff --git a/cmd/flipt/cloud.go b/cmd/flipt/cloud.go index fa0591b6a8..99dba9758e 100644 --- a/cmd/flipt/cloud.go +++ b/cmd/flipt/cloud.go @@ -151,7 +151,7 @@ func (c *cloudCommand) login(cmd *cobra.Command, args []string) error { return fmt.Errorf("waiting for token: %w", err) } - if err := flow.Close(); err != nil { + if err := flow.Close(); err != nil && !errors.Is(err, net.ErrClosed) { return fmt.Errorf("closing flow: %w", err) } diff --git a/internal/server/authn/middleware/grpc/middleware.go b/internal/server/authn/middleware/grpc/middleware.go index 276c4eea89..1ba9263789 100644 --- a/internal/server/authn/middleware/grpc/middleware.go +++ b/internal/server/authn/middleware/grpc/middleware.go @@ -212,6 +212,10 @@ func JWTAuthenticationInterceptor(logger *zap.Logger, validator jwt.Validator, e } } + if iss, ok := jwtClaims["iss"].(string); ok { + metadata["io.flipt.auth.jwt.issuer"] = iss + } + auth := &authrpc.Authentication{ Method: authrpc.Method_METHOD_JWT, Metadata: metadata, diff --git a/internal/server/authn/middleware/grpc/middleware_test.go b/internal/server/authn/middleware/grpc/middleware_test.go index c0997a061b..feb60756c2 100644 --- a/internal/server/authn/middleware/grpc/middleware_test.go +++ b/internal/server/authn/middleware/grpc/middleware_test.go @@ -71,7 +71,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { name: "successful authentication", metadataFunc: func() metadata.MD { claims := map[string]interface{}{ - "iss": "https://flipt.io/", + "iss": "flipt.io", "aud": "flipt", "sub": "sunglasses", "iat": nowUnix, @@ -84,7 +84,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { } }, expectedJWT: jwt.Expected{ - Issuer: "https://flipt.io/", + Issuer: "flipt.io", Audiences: []string{"flipt"}, Subject: "sunglasses", }, @@ -93,7 +93,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { name: "successful authentication (with custom user claims)", metadataFunc: func() metadata.MD { claims := map[string]interface{}{ - "iss": "https://flipt.io/", + "iss": "flipt.io", "aud": "flipt", "iat": nowUnix, "exp": futureUnix, @@ -111,7 +111,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { } }, expectedJWT: jwt.Expected{ - Issuer: "https://flipt.io/", + Issuer: "flipt.io", Audiences: []string{"flipt"}, }, expectedMetadata: map[string]string{ @@ -119,13 +119,14 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { "io.flipt.auth.jwt.email": "email", "io.flipt.auth.jwt.picture": "image", "io.flipt.auth.jwt.name": "name", + "io.flipt.auth.jwt.issuer": "flipt.io", }, }, { name: "invalid issuer", metadataFunc: func() metadata.MD { claims := map[string]interface{}{ - "iss": "https://foo.com/", + "iss": "foo.com", "iat": nowUnix, "exp": futureUnix, } @@ -136,7 +137,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { } }, expectedJWT: jwt.Expected{ - Issuer: "https://flipt.io/", + Issuer: "flipt.io", }, expectedErr: ErrUnauthenticated, }, @@ -144,7 +145,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { name: "invalid subject", metadataFunc: func() metadata.MD { claims := map[string]interface{}{ - "iss": "https://flipt.io/", + "iss": "flipt.io", "iat": nowUnix, "exp": futureUnix, "sub": "bar", @@ -156,7 +157,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { } }, expectedJWT: jwt.Expected{ - Issuer: "https://flipt.io/", + Issuer: "flipt.io", Subject: "flipt", }, expectedErr: ErrUnauthenticated, @@ -165,7 +166,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { name: "invalid audience", metadataFunc: func() metadata.MD { claims := map[string]interface{}{ - "iss": "https://flipt.io/", + "iss": "flipt.io", "iat": nowUnix, "exp": futureUnix, "aud": "bar", @@ -177,7 +178,7 @@ func TestJWTAuthenticationInterceptor(t *testing.T) { } }, expectedJWT: jwt.Expected{ - Issuer: "https://flipt.io/", + Issuer: "flipt.io", Audiences: []string{"flipt"}, }, expectedErr: ErrUnauthenticated, diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index 2922997bdf..dfd0d1c9ad 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -39,9 +39,7 @@ export default function Header(props: HeaderProps) { {info && info.updateAvailable && } {/* user profile */} - {session && session.self && ( - - )} + {session && session.self && } diff --git a/ui/src/components/header/UserProfile.tsx b/ui/src/components/header/UserProfile.tsx index a12a3c5980..917f826f0e 100644 --- a/ui/src/components/header/UserProfile.tsx +++ b/ui/src/components/header/UserProfile.tsx @@ -4,63 +4,65 @@ import { Fragment } from 'react'; import { expireAuthSelf } from '~/data/api'; import { useError } from '~/data/hooks/error'; import { useSession } from '~/data/hooks/session'; -import { IAuthMethodGithubMetadata } from '~/types/auth/Github'; -import { IAuthMethodJWTMetadata } from '~/types/auth/JWT'; -import { IAuthMethodOIDCMetadata } from '~/types/auth/OIDC'; +import { IAuthGithubInternal } from '~/types/auth/Github'; +import { IAuthJWTInternal } from '~/types/auth/JWT'; +import { IAuthOIDCInternal } from '~/types/auth/OIDC'; import { cls } from '~/utils/helpers'; type UserProfileProps = { - metadata?: - | IAuthMethodOIDCMetadata - | IAuthMethodGithubMetadata - | IAuthMethodJWTMetadata; + session?: IAuthOIDCInternal | IAuthGithubInternal | IAuthJWTInternal; }; export default function UserProfile(props: UserProfileProps) { - const { metadata } = props; - + const { session } = props; const { setError } = useError(); const { clearSession } = useSession(); let name: string | undefined; let login: string | undefined; let imgURL: string | undefined; + let logoutURL = '/'; - if (metadata) { - // TODO: dry this up - if ('io.flipt.auth.github.name' in metadata) { - name = metadata['io.flipt.auth.github.name'] ?? 'User'; - if (metadata['io.flipt.auth.github.picture']) { - imgURL = metadata['io.flipt.auth.github.picture']; - } - if (metadata['io.flipt.auth.github.preferred_username']) { - login = metadata['io.flipt.auth.github.preferred_username']; - } - } else if ('io.flipt.auth.oidc.name' in metadata) { - name = metadata['io.flipt.auth.oidc.name'] ?? 'User'; - if (metadata['io.flipt.auth.oidc.picture']) { - imgURL = metadata['io.flipt.auth.oidc.picture']; + if (session) { + const authMethods = ['github', 'oidc', 'jwt']; + const authMethod = authMethods.find( + (method) => `METHOD_${method.toLocaleUpperCase()}` === session.method + ); + + if (authMethod) { + const metadata = session.metadata; + + const authMethodNameKey = `io.flipt.auth.${authMethod}.name`; + name = metadata[authMethodNameKey as keyof typeof metadata] ?? 'User'; + + const authMethodPictureKey = `io.flipt.auth.${authMethod}.picture`; + if (metadata[authMethodPictureKey as keyof typeof metadata]) { + imgURL = metadata[authMethodPictureKey as keyof typeof metadata]; } - if (metadata['io.flipt.auth.oidc.preferred_username']) { - login = metadata['io.flipt.auth.oidc.preferred_username']; + + const authMethodPreferredUsernameKey = `io.flipt.auth.${authMethod}.preferred_username`; + if (metadata[authMethodPreferredUsernameKey as keyof typeof metadata]) { + login = + metadata[authMethodPreferredUsernameKey as keyof typeof metadata]; } - } else if ('io.flipt.auth.jwt.name' in metadata) { - name = metadata['io.flipt.auth.jwt.name'] ?? 'User'; - if (metadata['io.flipt.auth.jwt.picture']) { - imgURL = metadata['io.flipt.auth.jwt.picture']; + + const authMethodIssuerKey = `io.flipt.auth.${authMethod}.issuer`; + if (metadata[authMethodIssuerKey as keyof typeof metadata]) { + const logoutURI = + metadata[authMethodIssuerKey as keyof typeof metadata]; + logoutURL = `//${logoutURI}`; } } } - const logout = async () => { - expireAuthSelf() - .then(() => { - clearSession(); - window.location.href = '/'; - }) - .catch((err) => { - setError(err); - }); + const logout = () => { + try { + expireAuthSelf(); + clearSession(); + window.location.href = logoutURL; + } catch (err) { + setError(err); + } }; return ( diff --git a/ui/src/types/auth/Github.ts b/ui/src/types/auth/Github.ts index 810eaa6ba2..d8152528b9 100644 --- a/ui/src/types/auth/Github.ts +++ b/ui/src/types/auth/Github.ts @@ -16,5 +16,6 @@ export interface IAuthMethodGithubMetadata { } export interface IAuthGithubInternal extends IAuth { + method: 'METHOD_GITHUB'; metadata: IAuthMethodGithubMetadata; } diff --git a/ui/src/types/auth/JWT.ts b/ui/src/types/auth/JWT.ts index db6a50d142..1d2c694c5a 100644 --- a/ui/src/types/auth/JWT.ts +++ b/ui/src/types/auth/JWT.ts @@ -12,8 +12,10 @@ export interface IAuthMethodJWTMetadata { 'io.flipt.auth.jwt.email'?: string; 'io.flipt.auth.jwt.name'?: string; 'io.flipt.auth.jwt.picture'?: string; + 'io.flipt.auth.jwt.issuer'?: string; } export interface IAuthJWTInternal extends IAuth { + method: 'METHOD_JWT'; metadata: IAuthMethodJWTMetadata; } diff --git a/ui/src/types/auth/OIDC.ts b/ui/src/types/auth/OIDC.ts index ee490495c4..4e7c31384e 100644 --- a/ui/src/types/auth/OIDC.ts +++ b/ui/src/types/auth/OIDC.ts @@ -22,5 +22,6 @@ export interface IAuthMethodOIDCMetadata { } export interface IAuthOIDCInternal extends IAuth { + method: 'METHOD_OIDC'; metadata: IAuthMethodOIDCMetadata; }