Skip to content

Commit

Permalink
add oidc implicit flow support
Browse files Browse the repository at this point in the history
  • Loading branch information
bgerrity committed Feb 23, 2024
1 parent bdf6de8 commit c548516
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 50 deletions.
13 changes: 12 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,17 @@ 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
callbackUrl: http://localhost:8080
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
27 changes: 21 additions & 6 deletions server/server/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ import (
)

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

type CodecResponse struct {
Expand Down Expand Up @@ -110,17 +117,25 @@ 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,
CallbackURL: authProviderCfg.CallbackURL,
Scopes: authProviderCfg.Scopes,
Options: options,
},
BannerText: cfg.BannerText,
DefaultNamespace: cfg.DefaultNamespace,
Expand Down
23 changes: 21 additions & 2 deletions server/server/config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,27 @@ 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")
}
case "implicit":
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")
}
default:
return errors.New("auth oidc flow is not valid")
}

if c.ClientID == "" {
Expand Down
14 changes: 9 additions & 5 deletions server/server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,16 @@ type (
Label string `yaml:"label"`
// 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"`
// 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"`
// URL for the callback, e.g. https://localhost:8080/sso/callback
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
7 changes: 7 additions & 0 deletions src/lib/services/settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ 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,
callbackUrl: settingsResponse?.Auth?.CallbackURL,
scopes: settingsResponse?.Auth?.Scopes,
options: settingsResponse?.Auth?.Options,
},
bannerText: settingsResponse?.BannerText,
Expand Down
16 changes: 13 additions & 3 deletions src/lib/stores/auth-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ 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:
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
12 changes: 12 additions & 0 deletions src/lib/types/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,21 @@ 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;
callbackUrl: string;
scopes: string[];
options: string[];
};
bannerText: string;
Expand Down
14 changes: 13 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,17 @@ 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;
CallbackURL: string;
Scopes: string[];
Options: string[];
};
BannerText: string;
Codec: {
Endpoint: string;
Expand Down
9 changes: 8 additions & 1 deletion src/lib/utilities/is-authorized.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { Settings, User } from '$lib/types/global';
import { OIDCFlow } from '$lib/types/global';

export const isAuthorized = (settings: Settings, user: User): boolean => {
return !settings.auth.enabled || Boolean(user?.accessToken);
return (
!settings.auth.enabled ||
Boolean(
(settings.auth.flow == OIDCFlow.AuthorizationCode && user?.accessToken) ||
(settings.auth.flow == OIDCFlow.Implicit && user?.idToken),
)
);
};
12 changes: 5 additions & 7 deletions src/lib/utilities/request-from-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,15 @@ const withAuth = async (
options: RequestInit,
isBrowser = BROWSER,
): Promise<RequestInit> => {
if (getAuthUser().accessToken) {
const authUser = getAuthUser();
if (authUser) {
const { accessToken, idToken } = authUser;
options.headers = await withBearerToken(
options?.headers,
async () => getAuthUser().accessToken,
isBrowser,
);
options.headers = withIdToken(
options?.headers,
getAuthUser().idToken,
async () => accessToken ?? idToken,
isBrowser,
);
options.headers = withIdToken(options?.headers, idToken, isBrowser);
} else if (globalThis?.AccessToken) {
options.headers = await withBearerToken(
options?.headers,
Expand Down
Loading

0 comments on commit c548516

Please sign in to comment.