Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for OIDC implicit flow #1879

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"json-bigint": "^1.0.0",
"just-debounce": "^1.1.0",
"just-throttle": "^4.2.0",
"jwt-decode": "^4.0.0",
"lodash.groupby": "^4.6.0",
"remark-stringify": "^10.0.3",
"sanitize-html": "^2.12.1",
Expand Down
10 changes: 9 additions & 1 deletion pnpm-lock.yaml

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

12 changes: 11 additions & 1 deletion server/config/development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ batchActionsDisabled: false
hideWorkflowQueryErrors: false
auth:
enabled: false
providers:
providers: # only the first is used. second is provided for reference
- label: Auth0 oidc # for internal use; in future may expose as button text
type: oidc # for futureproofing; only oidc is supported today
providerUrl: https://myorg.us.auth0.com/
Expand All @@ -36,6 +36,16 @@ auth:
audience: myorg-dev
organization: org_xxxxxxxxxxxx
invitation:
- label: oidc implicit flow
type: oidc
flow: implicit
# TODO: support optional issuer validation
authorizationUrl: https://accounts.google.com/o/oauth2/v2/auth # discovery isn't supported for implicit flow. the endpoint must be provided directly
clientId: xxxxxxxxxxxxxxxxxxxx
scopes:
- openid
- profile
- email
tls:
caFile:
certFile:
Expand Down
1 change: 1 addition & 0 deletions server/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ docker run \
-e TEMPORAL_ADDRESS=127.0.0.1:7233 \
-e TEMPORAL_UI_PORT=8080 \
-e TEMPORAL_AUTH_ENABLED=true \
-e TEMPORAL_AUTH_FLOW_TYPE=authorization-code \
-e TEMPORAL_AUTH_PROVIDER_URL=https://accounts.google.com \
-e TEMPORAL_AUTH_CLIENT_ID=xxxxx-xxxx.apps.googleusercontent.com \
-e TEMPORAL_AUTH_CLIENT_SECRET=xxxxxxxxxxxxxxx \
Expand Down
2 changes: 2 additions & 0 deletions server/docker/config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ auth:
providers:
- label: {{ default .Env.TEMPORAL_AUTH_LABEL "sso" }}
type: {{ default .Env.TEMPORAL_AUTH_TYPE "oidc" }}
flow: {{ default .Env.TEMPORAL_AUTH_FLOW_TYPE "authorization-code" }}
providerUrl: {{ .Env.TEMPORAL_AUTH_PROVIDER_URL }}
issuerUrl: {{ default .Env.TEMPORAL_AUTH_ISSUER_URL "" }}
authorizationUrl:: {{ default .Env.TEMPORAL_AUTH_AUTHORIZATION_URL "" }}
clientId: {{ .Env.TEMPORAL_AUTH_CLIENT_ID }}
clientSecret: {{ .Env.TEMPORAL_AUTH_CLIENT_SECRET }}
callbackUrl: {{ .Env.TEMPORAL_AUTH_CALLBACK_URL }}
Expand Down
25 changes: 19 additions & 6 deletions server/server/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ import (
)

type Auth struct {
Enabled bool
Options []string
Enabled bool
Flow string
ProviderURL string
IssuerURL string
AuthorizationURL string
ClientID string
Scopes []string
Options []string
}

type CodecResponse struct {
Expand Down Expand Up @@ -110,17 +116,24 @@ func GetSettings(cfgProvider *config.ConfigProviderWithRefresh) func(echo.Contex
}

var options []string
var authProviderCfg config.AuthProvider
if len(cfg.Auth.Providers) != 0 {
authProviderCfg := cfg.Auth.Providers[0].Options
for k := range authProviderCfg {
authProviderCfg = cfg.Auth.Providers[0]
for k := range authProviderCfg.Options {
options = append(options, k)
}
}

settings := &SettingsResponse{
Auth: &Auth{
Enabled: cfg.Auth.Enabled,
Options: options,
Enabled: cfg.Auth.Enabled,
Flow: authProviderCfg.Flow,
ProviderURL: authProviderCfg.ProviderURL,
IssuerURL: authProviderCfg.IssuerURL,
AuthorizationURL: authProviderCfg.AuthorizationURL,
ClientID: authProviderCfg.ClientID,
Scopes: authProviderCfg.Scopes,
Options: options,
},
BannerText: cfg.BannerText,
DefaultNamespace: cfg.DefaultNamespace,
Expand Down
35 changes: 29 additions & 6 deletions server/server/config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,40 @@ func (c *AuthProvider) validate() error {
return nil
}

if c.ProviderURL == "" {
return errors.New("auth provider url is not")
switch flowType := c.Flow; flowType {
case "authorization-code":
if c.ProviderURL == "" {
return errors.New("auth provider url is not set")
}
if c.AuthorizationURL != "" {
return errors.New("auth endpoint url is not used in auth code flow")
}
if c.CallbackURL == "" {
return errors.New("auth callback url is not set")
}
case "implicit":
// TODO: support oidc discovery in implicit flow
if c.ProviderURL != "" {
return errors.New("auth provider url is not used in implicit flow")
}
// TODO: support optional issuer validation
if c.AuthorizationURL == "" {
return errors.New("auth issuer url is not set")
}
if c.ClientSecret != "" {
return errors.New("no secrets in implicit flow")
}
if c.CallbackURL != "" {
return errors.New("auth callback url is not used in implicit flow")
}

default:
return errors.New("auth oidc flow is not valid")
}

if c.ClientID == "" {
return errors.New("auth client id is not set")
}

if c.CallbackURL == "" {
return errors.New("auth callback url is not set")
}

return nil
}
20 changes: 12 additions & 8 deletions server/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,23 @@ type (
}

AuthProvider struct {
// Label - optional label for the provider
// Optional. Label for the provider.
Label string `yaml:"label"`
// Type of the auth provider. Only OIDC is supported today
// Type of the auth provider. Only OIDC is supported today.
Type string `yaml:"type"`
// OIDC .well-known/openid-configuration URL, ex. https://accounts.google.com/
// OIDC login flow type. The "authorization-code" and "implicit" flows are supported.
Flow string `yaml:"flow"`
// OIDC .well-known/openid-configuration URL, e.g. https://accounts.google.com/. Discovery unsupported in implicit flow.
ProviderURL string `yaml:"providerUrl"`
// IssuerUrl - optional. Needed only when differs from the auth provider URL
IssuerUrl string `yaml:"issuerUrl"`
ClientID string `yaml:"clientId"`
ClientSecret string `yaml:"clientSecret"`
// Optional. Needed only when differs from the auth provider URL. In implicit flow, enables token issuer validation.
IssuerURL string `yaml:"issuerUrl"`
// Required for implicit flow. OIDC authorization endpoint URL, e.g. https://accounts.google.com/o/oauth2/v2/auth.
AuthorizationURL string `yaml:"authorizationUrl"`
ClientID string `yaml:"clientId"`
ClientSecret string `yaml:"clientSecret"`
// Scopes for auth. Typically [openid, profile, email]
Scopes []string `yaml:"scopes"`
// CallbackURL - URL for the callback URL, ex. https://localhost:8080/sso/callback
// URL for the callback, e.g. https://localhost:8080/sso/callback. Not used in the implicit flow.
CallbackURL string `yaml:"callbackUrl"`
// Options added as URL query params when redirecting to auth provider. Can be used to configure custom auth flows such as Auth0 invitation flow.
Options map[string]interface{} `yaml:"options"`
Expand Down
47 changes: 29 additions & 18 deletions server/server/route/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,37 @@ func SetAuthRoutes(e *echo.Echo, cfgProvider *config.ConfigProviderWithRefresh)

providerCfg := serverCfg.Auth.Providers[0] // only single provider is currently supported

if len(providerCfg.IssuerUrl) > 0 {
ctx = oidc.InsecureIssuerURLContext(ctx, providerCfg.IssuerUrl)
}
provider, err := oidc.NewProvider(ctx, providerCfg.ProviderURL)
if err != nil {
log.Fatal(err)
}
api := e.Group("/auth")
switch providerCfg.Flow {
case "authorization-code":
if len(providerCfg.IssuerURL) > 0 {
ctx = oidc.InsecureIssuerURLContext(ctx, providerCfg.IssuerURL)
}

oauthCfg := oauth2.Config{
ClientID: providerCfg.ClientID,
ClientSecret: providerCfg.ClientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: providerCfg.CallbackURL,
Scopes: providerCfg.Scopes,
}
if len(providerCfg.AuthorizationURL) > 0 {
log.Fatal(`authorization url should not be set for auth code flow`)
}

api := e.Group("/auth")
api.GET("/sso", authenticate(&oauthCfg, providerCfg.Options))
api.GET("/sso/callback", authenticateCb(ctx, &oauthCfg, provider))
api.GET("/sso_callback", authenticateCb(ctx, &oauthCfg, provider)) // compatibility with UI v1
provider, err := oidc.NewProvider(ctx, providerCfg.ProviderURL)
if err != nil {
log.Fatal(err)
}

oauthCfg := oauth2.Config{
ClientID: providerCfg.ClientID,
ClientSecret: providerCfg.ClientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: providerCfg.CallbackURL,
Scopes: providerCfg.Scopes,
}

api.GET("/sso", authenticate(&oauthCfg, providerCfg.Options))
api.GET("/sso/callback", authenticateCb(ctx, &oauthCfg, provider))
api.GET("/sso_callback", authenticateCb(ctx, &oauthCfg, provider)) // compatibility with UI v1
case "implicit":
// The implicit flow is principally designed for single-page applications.
// Fully delegated to the client.
}
}

func authenticate(config *oauth2.Config, options map[string]interface{}) func(echo.Context) error {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/top-nav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
{#if showNamespaceSpecificNav}
<DataEncoderStatus />
{/if}
{#if $authUser.accessToken}
{#if $authUser.idToken || $authUser.accessToken}
<MenuContainer>
<MenuButton variant="ghost" hasIndicator controls="user-menu">
<img
Expand Down
6 changes: 6 additions & 0 deletions src/lib/services/settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const fetchSettings = async (request = fetch): Promise<Settings> => {
const settingsInformation = {
auth: {
enabled: !!settingsResponse?.Auth?.Enabled,
flow: settingsResponse?.Auth?.Flow,
providerUrl: settingsResponse?.Auth?.ProviderURL,
issuerUrl: settingsResponse?.Auth?.IssuerURL,
authorizationUrl: settingsResponse?.Auth?.AuthorizationURL,
clientId: settingsResponse?.Auth?.ClientID,
scopes: settingsResponse?.Auth?.Scopes,
options: settingsResponse?.Auth?.Options,
},
bannerText: settingsResponse?.BannerText,
Expand Down
17 changes: 14 additions & 3 deletions src/lib/stores/auth-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ import { get } from 'svelte/store';

import { persistStore } from '$lib/stores/persist-store';
import type { User } from '$lib/types/global';
import { OIDCFlow } from '$lib/types/global';

export const authUser = persistStore<User>('AuthUser', {});

export const getAuthUser = (): User => get(authUser);

export const setAuthUser = (user: User) => {
export const setAuthUser = (user: User, oidcFlow: OIDCFlow) => {
const { accessToken, idToken, name, email, picture } = user;

if (!accessToken) {
throw new Error('No access token');
switch (oidcFlow) {
case OIDCFlow.AuthorizationCode:
default:
if (!accessToken) {
throw new Error('No access token');
}
break;
case OIDCFlow.Implicit:
if (!idToken) {
throw new Error('No id token');
}
break;
}

authUser.set({
Expand Down
11 changes: 11 additions & 0 deletions src/lib/types/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,20 @@ export interface NetworkError {
message?: string;
}

export enum OIDCFlow {
AuthorizationCode = 'authorization-code',
Implicit = 'implicit',
}

export type Settings = {
auth: {
enabled: boolean;
flow: OIDCFlow;
providerUrl: string;
issuerUrl: string;
authorizationUrl: string;
clientId: string;
scopes: string[];
options: string[];
};
bannerText: string;
Expand Down
13 changes: 12 additions & 1 deletion src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { google, temporal } from '@temporalio/proto';

import type { OIDCFlow } from '$lib/types/global';

// api.workflowservice

export type DescribeNamespaceResponse =
Expand Down Expand Up @@ -200,7 +202,16 @@ export type Timestamp = google.protobuf.ITimestamp;

// extra APIs
export type SettingsResponse = {
Auth: { Enabled: boolean; Options: string[] };
Auth: {
Enabled: boolean;
Flow: OIDCFlow;
ProviderURL: string;
IssuerURL: string;
AuthorizationURL: string;
ClientID: string;
Scopes: string[];
Options: string[];
};
BannerText: string;
Codec: {
Endpoint: string;
Expand Down
Loading