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

Support nonce and acr with OIDC + Tests #883

Merged
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6ca8610
First unit test for OIDCConfig
fflorent Feb 27, 2024
88a51f8
WIP
fflorent Feb 27, 2024
5357143
WIP
fflorent Feb 29, 2024
4186544
WIP
fflorent Feb 29, 2024
1c99806
More tests
fflorent Mar 5, 2024
296b0ee
More tests, finish implementation
fflorent Mar 5, 2024
40b589b
Test getLogoutRedirectUrl
fflorent Mar 6, 2024
db0c0a2
Minor changes
fflorent Mar 6, 2024
bb3874a
WIP: LoginWithOIDC
fflorent Mar 6, 2024
faad33d
Log err.response if present
fflorent Mar 7, 2024
e158b9e
Add support for extra OIDC client metadata
fflorent Mar 7, 2024
e7a689e
Log userinfo in debug and check passed info in endSessionUrl
fflorent Mar 7, 2024
38128ac
Many other stuffs + cleanup + working version with Agent Connect
fflorent Mar 7, 2024
4038f11
WIP: integration tests
fflorent Mar 13, 2024
974a1ff
Improve formatTokenForLogs + format file
fflorent Mar 19, 2024
4bfdc32
Selenium tests working
fflorent Mar 19, 2024
b2bba75
Fix CI?
fflorent Mar 19, 2024
92e25ec
Tests with keycloak are not in test:server
fflorent Mar 19, 2024
f0f606f
Fix CI ? + Skip in LoginWithOIDC
fflorent Mar 19, 2024
f03bba1
Remove health-checks for keycloak, curl being not avaible in container
fflorent Mar 19, 2024
6c59ea9
Attempt fix using docker-run-action
fflorent Mar 19, 2024
b859801
Remove integration tests (for now)
fflorent Mar 20, 2024
40d0047
Add friendly page for login failure
fflorent Mar 26, 2024
2a807ba
Fix bad indentation
fflorent Mar 27, 2024
3c9be93
PR remark
fflorent Mar 27, 2024
f2457ef
Fix after rebase
fflorent May 29, 2024
6ba47c4
PR remarks
fflorent May 29, 2024
0a0ec36
Adapt comment
fflorent Jun 5, 2024
2050772
Smarter way to test whether an info log has been called
fflorent Jun 5, 2024
3a5403b
Fix typo in documentation
fflorent Jun 18, 2024
1e6b8dd
Remove underscore from implementation
fflorent Jun 18, 2024
ac44010
Don't accept empty string for GRIST_OIDC_IDP_ENABLED_PROTECTIONS
fflorent Jun 18, 2024
bd99800
Raise a warning when protections are disabled.
fflorent Jun 18, 2024
707653f
Introduce ProtectionManager and clear state from session after login
fflorent Jun 20, 2024
74ecd38
Various remarks
fflorent Jul 9, 2024
9c51d15
PR remarks
fflorent Jul 17, 2024
5c2135e
Improve session management in callback and translate error message
fflorent Jul 18, 2024
35f76cc
Comment what is idToken + reuse "sign in" button
fflorent Jul 18, 2024
3607a61
Document VERBOSE=1 tip when running the tests
fflorent Jul 18, 2024
093f0e2
Improve formatTokenForLogs
fflorent Jul 23, 2024
bb6445b
Dmitri's remarks
fflorent Jul 25, 2024
15732bc
Better error logging of responses for OIDC callback
fflorent Jul 25, 2024
e40fcbd
Styling: Fix missing semi-colon
fflorent Jul 29, 2024
e2eeaa1
PR remarks from Spoffy
fflorent Aug 8, 2024
360f05c
Rephrase comment
fflorent Aug 8, 2024
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
23 changes: 20 additions & 3 deletions app/client/ui/errorPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ const testId = makeTestId('test-');

const t = makeT('errorPages');

function signInAgainButton() {
return cssButtonWrap(bigPrimaryButtonLink(
t("Sign in again"), {href: getLoginUrl()}, testId('error-signin')
));
}

export function createErrPage(appModel: AppModel) {
const {errMessage, errPage} = getGristConfig();
return errPage === 'signed-out' ? createSignedOutPage(appModel) :
errPage === 'not-found' ? createNotFoundPage(appModel, errMessage) :
errPage === 'access-denied' ? createForbiddenPage(appModel, errMessage) :
errPage === 'account-deleted' ? createAccountDeletedPage(appModel) :
errPage === 'signin-failed' ? createSigninFailedPage(appModel, errMessage) :
createOtherErrorPage(appModel, errMessage);
}

Expand Down Expand Up @@ -61,9 +68,7 @@ export function createSignedOutPage(appModel: AppModel) {

return pagePanelsError(appModel, t("Signed out{{suffix}}", {suffix: ''}), [
cssErrorText(t("You are now signed out.")),
cssButtonWrap(bigPrimaryButtonLink(
t("Sign in again"), {href: getLoginUrl()}, testId('error-signin')
))
signInAgainButton(),
]);
}

Expand Down Expand Up @@ -98,6 +103,18 @@ export function createNotFoundPage(appModel: AppModel, message?: string) {
]);
}

export function createSigninFailedPage(appModel: AppModel, message?: string) {
document.title = t("Sign-in failed{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())});
return pagePanelsError(appModel, t("Sign-in failed{{suffix}}", {suffix: ''}), [
cssErrorText(message ??
t("Failed to log in.{{separator}}Please try again or contact support.", {
separator: dom('br')
})),
signInAgainButton(),
cssButtonWrap(bigBasicButtonLink(t("Contact support"), {href: commonUrls.contactSupport})),
]);
}

/**
* Creates a generic error page with the given message.
*/
Expand Down
10 changes: 8 additions & 2 deletions app/common/StringUnion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export class StringUnionError extends TypeError {
constructor(errMessage: string, public readonly actual: string, public readonly values: string[]) {
super(errMessage);
}
}

/**
* TypeScript will infer a string union type from the literal values passed to
* this function. Without `extends string`, it would instead generalize them
Expand Down Expand Up @@ -28,7 +34,7 @@ export const StringUnion = <UnionType extends string>(...values: UnionType[]) =>
if (!guard(value)) {
const actual = JSON.stringify(value);
const expected = values.map(s => JSON.stringify(s)).join(' | ');
throw new TypeError(`Value '${actual}' is not assignable to type '${expected}'.`);
throw new StringUnionError(`Value '${actual}' is not assignable to type '${expected}'.`, actual, values);
}
return value;
};
Expand All @@ -44,6 +50,6 @@ export const StringUnion = <UnionType extends string>(...values: UnionType[]) =>
return value != null && guard(value) ? value : undefined;
};

const unionNamespace = {guard, check, parse, values, checkAll};
const unionNamespace = { guard, check, parse, values, checkAll };
return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};
21 changes: 15 additions & 6 deletions app/server/lib/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,21 @@ export interface SessionObj {
// something they just added, without allowing the suer
// to edit other people's contributions).

oidc?: {
// codeVerifier is used during OIDC authentication, to protect against attacks like CSRF.
codeVerifier?: string;
state?: string;
targetUrl?: string;
}
oidc?: SessionOIDCInfo;
}

export interface SessionOIDCInfo {
// more details on protections are available here: https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#special-case-error-responses
// code_verifier is used during OIDC authentication for PKCE protection, to protect against attacks like CSRF.
// PKCE + state are currently the best combination to protect against CSRF and code injection attacks.
code_verifier?: string;
// much like code_verifier, for OIDC providers that do not support PKCE.
nonce?: string;
// state is used to protect against Error Responses spoofs.
state?: string;
targetUrl?: string;
// Stores user claims signed by the issuer, store it to allow loging out.
idToken?: string;
}

// Make an artificial change to a session to encourage express-session to set a cookie.
Expand Down
12 changes: 7 additions & 5 deletions app/server/lib/FlexServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ export class FlexServer implements GristServer {
server: this,
staticDir: getAppPathTo(this.appRoot, 'static'),
tag: this.tag,
testLogin: allowTestLogin(),
testLogin: isTestLoginAllowed(),
baseDomain: this._defaultBaseDomain,
});

Expand Down Expand Up @@ -1206,7 +1206,7 @@ export class FlexServer implements GristServer {
})));
this.app.get('/signin', ...signinMiddleware, expressWrap(this._redirectToLoginOrSignup.bind(this, {})));

if (allowTestLogin()) {
if (isTestLoginAllowed()) {
// This is an endpoint for the dev environment that lets you log in as anyone.
// For a standard dev environment, it will be accessible at localhost:8080/test/login
// and localhost:8080/o/<org>/test/login. Only available when GRIST_TEST_LOGIN is set.
Expand Down Expand Up @@ -1976,7 +1976,9 @@ export class FlexServer implements GristServer {
}

public resolveLoginSystem() {
return process.env.GRIST_TEST_LOGIN ? getTestLoginSystem() : (this._getLoginSystem?.() || getLoginSystem());
return isTestLoginAllowed() ?
getTestLoginSystem() :
(this._getLoginSystem?.() || getLoginSystem());
}

public addUpdatesCheck() {
Expand Down Expand Up @@ -2506,8 +2508,8 @@ function configServer<T extends https.Server|http.Server>(server: T): T {
}

// Returns true if environment is configured to allow unauthenticated test logins.
function allowTestLogin() {
return Boolean(process.env.GRIST_TEST_LOGIN);
function isTestLoginAllowed() {
return isAffirmative(process.env.GRIST_TEST_LOGIN);
}

// Check OPTIONS requests for allowed origins, and return heads to allow the browser to proceed
Expand Down
Loading
Loading