Skip to content

Commit 5ebe1cc

Browse files
authored
Add PKCE support for OIDC (#20094)
1 parent c4b53f9 commit 5ebe1cc

File tree

14 files changed

+1256
-1031
lines changed

14 files changed

+1256
-1031
lines changed

components/dashboard/src/dedicated-setup/SSOSetupStep.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const SSOSetupStep: FC<Props> = ({ config, onComplete, progressCurrent, p
2828
clientId: config?.oauth2Config?.clientId ?? "",
2929
clientSecret: config?.oauth2Config?.clientSecret ?? "",
3030
celExpression: config?.oauth2Config?.celExpression ?? "",
31+
usePKCE: config?.oauth2Config?.usePkce ?? false,
3132
});
3233
const configIsValid = isValid(ssoConfig);
3334

components/dashboard/src/teams/sso/OIDCClientConfigModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const OIDCClientConfigModal: FC<Props> = ({ clientConfig, onSaved, onClos
2727
clientId: clientConfig?.oauth2Config?.clientId ?? "",
2828
clientSecret: clientConfig?.oauth2Config?.clientSecret ?? "",
2929
celExpression: clientConfig?.oauth2Config?.celExpression ?? "",
30+
usePKCE: clientConfig?.oauth2Config?.usePkce ?? false,
3031
});
3132
const configIsValid = isValid(ssoConfig);
3233

components/dashboard/src/teams/sso/SSOConfigForm.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import isURL from "validator/lib/isURL";
1414
import { useCurrentOrg } from "../../data/organizations/orgs-query";
1515
import { useUpsertOIDCClientMutation } from "../../data/oidc-clients/upsert-oidc-client-mutation";
1616
import { Subheading } from "../../components/typography/headings";
17+
import { CheckboxInputField } from "../../components/forms/CheckboxInputField";
1718

1819
type Props = {
1920
config: SSOConfig;
@@ -72,6 +73,13 @@ export const SSOConfigForm: FC<Props> = ({ config, readOnly = false, onChange })
7273
onChange={(val) => onChange({ clientSecret: val })}
7374
/>
7475

76+
<CheckboxInputField
77+
label="Use PKCE"
78+
checked={config.usePKCE}
79+
disabled={readOnly}
80+
onChange={(val) => onChange({ usePKCE: val })}
81+
/>
82+
7583
<Subheading className="mt-8">
7684
<strong>3.</strong> Restrict available accounts in your Identity Providers.
7785
<a
@@ -103,6 +111,7 @@ export type SSOConfig = {
103111
clientId: string;
104112
clientSecret: string;
105113
celExpression?: string;
114+
usePKCE: boolean;
106115
};
107116

108117
export const ssoConfigReducer = (state: SSOConfig, action: Partial<SSOConfig>) => {
@@ -155,6 +164,7 @@ export const useSaveSSOConfig = () => {
155164
clientId: trimmedClientId,
156165
clientSecret: trimmedClientSecret,
157166
celExpression: trimmedCelExpression,
167+
usePkce: ssoConfig.usePKCE,
158168
},
159169
oidcConfig: {
160170
issuer: trimmedIssuer,
@@ -168,6 +178,7 @@ export const useSaveSSOConfig = () => {
168178
// TODO: determine how we should handle when user doesn't change their secret
169179
clientSecret: trimmedClientSecret.toLowerCase() === "redacted" ? "" : trimmedClientSecret,
170180
celExpression: trimmedCelExpression,
181+
usePkce: ssoConfig.usePKCE,
171182
},
172183
oidcConfig: {
173184
issuer: trimmedIssuer,

components/gitpod-db/go/oidc_client_config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ type OIDCSpec struct {
6060

6161
// CelExpression is an optional expression that can be used to determine if the client should be allowed to authenticate.
6262
CelExpression string `json:"celExpression"`
63+
64+
// UsePKCE specifies if the client should use PKCE for the OAuth flow.
65+
UsePKCE bool `json:"usePKCE"`
6366
}
6467

6568
func CreateOIDCClientConfig(ctx context.Context, conn *gorm.DB, cfg OIDCClientConfig) (OIDCClientConfig, error) {
@@ -352,6 +355,7 @@ func partialUpdateOIDCSpec(old, new OIDCSpec) OIDCSpec {
352355
}
353356

354357
old.CelExpression = new.CelExpression
358+
old.UsePKCE = new.UsePKCE
355359

356360
if !oidcScopesEqual(old.Scopes, new.Scopes) {
357361
old.Scopes = new.Scopes

components/public-api-server/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ require (
2929
github.com/stretchr/testify v1.8.4
3030
github.com/stripe/stripe-go/v72 v72.122.0
3131
github.com/zitadel/oidc v1.13.0
32-
golang.org/x/oauth2 v0.7.0
32+
golang.org/x/oauth2 v0.22.0
3333
google.golang.org/grpc v1.57.0
3434
google.golang.org/protobuf v1.33.0
3535
gopkg.in/square/go-jose.v2 v2.6.0

components/public-api-server/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/public-api-server/pkg/apiv1/oidc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ func dbOIDCClientConfigToAPI(config db.OIDCClientConfig, decryptor db.Decryptor)
443443
AuthorizationEndpoint: decrypted.RedirectURL,
444444
Scopes: decrypted.Scopes,
445445
CelExpression: decrypted.CelExpression,
446+
UsePkce: decrypted.UsePKCE,
446447
},
447448
OidcConfig: &v1.OIDCConfig{
448449
Issuer: config.Issuer,
@@ -472,6 +473,7 @@ func toDbOIDCSpec(oauth2Config *v1.OAuth2Config) db.OIDCSpec {
472473
ClientID: oauth2Config.GetClientId(),
473474
ClientSecret: oauth2Config.GetClientSecret(),
474475
CelExpression: oauth2Config.GetCelExpression(),
476+
UsePKCE: oauth2Config.GetUsePkce(),
475477
RedirectURL: oauth2Config.GetAuthorizationEndpoint(),
476478
Scopes: append([]string{goidc.ScopeOpenID, "profile", "email"}, oauth2Config.GetScopes()...),
477479
}

components/public-api-server/pkg/oidc/oauth2.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,18 @@ func (s *Service) OAuth2Middleware(next http.Handler) http.Handler {
8484
return
8585
}
8686

87+
opts := []oauth2.AuthCodeOption{}
88+
if config.UsePKCE {
89+
codeVerifier, err := r.Cookie(verifierCookieName)
90+
if err != nil {
91+
http.Error(rw, "code_verifier cookie not found", http.StatusBadRequest)
92+
return
93+
}
94+
opts = append(opts, oauth2.VerifierOption(codeVerifier.Value))
95+
}
96+
8797
config.OAuth2Config.RedirectURL = getCallbackURL(r.Host)
88-
oauth2Token, err := config.OAuth2Config.Exchange(r.Context(), code)
98+
oauth2Token, err := config.OAuth2Config.Exchange(r.Context(), code, opts...)
8999
if err != nil {
90100
log.WithError(err).Warn("Failed to exchange OAuth2 token.")
91101
respondeWithError(rw, r, "Failed to exchange OAuth2 token: "+err.Error(), http.StatusInternalServerError, useHttpErrors)

components/public-api-server/pkg/oidc/router.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ func Router(s *Service) *chi.Mux {
3232
}
3333

3434
const (
35-
stateCookieName = "state"
36-
nonceCookieName = "nonce"
35+
stateCookieName = "state"
36+
nonceCookieName = "nonce"
37+
verifierCookieName = "verifier"
3738
)
3839

3940
func (s *Service) getStartHandler() http.HandlerFunc {
@@ -83,6 +84,7 @@ func (s *Service) getStartHandler() http.HandlerFunc {
8384

8485
http.SetCookie(rw, newCallbackCookie(r, nonceCookieName, startParams.Nonce))
8586
http.SetCookie(rw, newCallbackCookie(r, stateCookieName, startParams.State))
87+
http.SetCookie(rw, newCallbackCookie(r, verifierCookieName, startParams.CodeVerifier))
8688

8789
http.Redirect(rw, r, startParams.AuthCodeURL, http.StatusTemporaryRedirect)
8890
}

components/public-api-server/pkg/oidc/service.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ type ClientConfig struct {
5252
OAuth2Config *oauth2.Config
5353
VerifierConfig *goidc.Config
5454
CelExpression string
55+
UsePKCE bool
5556
}
5657

5758
type StartParams struct {
58-
State string
59-
Nonce string
60-
AuthCodeURL string
59+
State string
60+
Nonce string
61+
CodeVerifier string
62+
AuthCodeURL string
6163
}
6264

6365
type AuthFlowResult struct {
@@ -92,12 +94,21 @@ func (s *Service) getStartParams(config *ClientConfig, redirectURL string, state
9294

9395
// Configuring `AuthCodeOption`s, e.g. nonce
9496
config.OAuth2Config.RedirectURL = redirectURL
95-
authCodeURL := config.OAuth2Config.AuthCodeURL(state, goidc.Nonce(nonce))
97+
98+
opts := []oauth2.AuthCodeOption{goidc.Nonce(nonce)}
99+
var verifier string
100+
if config.UsePKCE {
101+
verifier = oauth2.GenerateVerifier()
102+
opts = append(opts, oauth2.S256ChallengeOption(verifier))
103+
}
104+
105+
authCodeURL := config.OAuth2Config.AuthCodeURL(state, opts...)
96106

97107
return &StartParams{
98-
AuthCodeURL: authCodeURL,
99-
State: state,
100-
Nonce: nonce,
108+
AuthCodeURL: authCodeURL,
109+
State: state,
110+
Nonce: nonce,
111+
CodeVerifier: verifier,
101112
}, nil
102113
}
103114

@@ -252,6 +263,7 @@ func (s *Service) convertClientConfig(ctx context.Context, dbEntry db.OIDCClient
252263
Scopes: spec.Scopes,
253264
},
254265
CelExpression: spec.CelExpression,
266+
UsePKCE: spec.UsePKCE,
255267
VerifierConfig: &goidc.Config{
256268
ClientID: spec.ClientID,
257269
},

0 commit comments

Comments
 (0)