Skip to content

Commit

Permalink
feat: added AUTH_REQUIRE_ELEVATED_CLAIM to require elevated permissio…
Browse files Browse the repository at this point in the history
…ns for certain actions
  • Loading branch information
dbarrosop committed Feb 3, 2024
1 parent fcec7fa commit 19e7582
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
| AUTH_WEBAUTHN_RP_ID | Relying party id. If not set `AUTH_CLIENT_URL` will be used as a default. | |
| AUTH_WEBAUTHN_RP_ORIGINS | Array of URLs where the registration is permitted and should have occurred on. `AUTH_CLIENT_URL` will be automatically added to the list of origins if is set. | |
| AUTH_WEBAUTHN_ATTESTATION_TIMEOUT | How long (in ms) the user can take to complete authentication. | `60000` (1 minute) |
| AUTH_REQUIRE_ELEVATED_CLAIM | Require x-hasura-auth-elevated claim to perform certain actions: create PATs, change email and/or password, enable/disable MFA and add security keys. If set to `recommended` the claim check is only performed if the user has a security key attached. If set to `required` the only action that won't require the claim is setting a security key for the first time. | `disabled` |

# OAuth environment variables

Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export const ERRORS = asErrors({
status: StatusCodes.UNAUTHORIZED,
message: 'User is not logged in',
},
'elevated-claim-required': {
status: StatusCodes.FORBIDDEN,
message: 'Elevated claim is required',
},
'forbidden-endpoint-in-production': {
status: StatusCodes.BAD_REQUEST,
message: 'This endpoint is only available on test environments',
Expand Down
42 changes: 35 additions & 7 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { RequestHandler } from 'express';
import { getPermissionVariables } from '@/utils';
import { sendError } from '@/errors';
import { ENV } from '../utils/env';
import { gqlSdk } from '@/utils';

export const authMiddleware: RequestHandler = async (req, _, next) => {
try {
Expand All @@ -11,17 +13,43 @@ export const authMiddleware: RequestHandler = async (req, _, next) => {
userId: permissionVariables['user-id'],
defaultRole: permissionVariables['default-role'],
isAnonymous: permissionVariables['is-anonymous'] === true,
elevated: permissionVariables['auth-elevated'] === permissionVariables['user-id'],
};
} catch (e) {
req.auth = null;
}
next();
};

export const authenticationGate: RequestHandler = (req, res, next) => {
if (!req.auth) {
return sendError(res, 'unauthenticated-user');
} else {
next();
}
};
export const authenticationGate = (checkElevatedPermissions: boolean, bypassIfNoKeys = false): RequestHandler => {
return async (req, res, next) => {
if (!req.auth) {
return sendError(res, 'unauthenticated-user');
}

if (!checkElevatedPermissions ||
ENV.AUTH_REQUIRE_ELEVATED_CLAIM === 'disabled' ||
!ENV.AUTH_WEBAUTHN_ENABLED) {
return next();
}

const auth = req.auth as RequestAuth;
const { authUserSecurityKeys } = await gqlSdk.getUserSecurityKeys({
id: auth.userId,
});

if (authUserSecurityKeys.length === 0 && ENV.AUTH_REQUIRE_ELEVATED_CLAIM === 'recommended') {
return next();
}

if (authUserSecurityKeys.length === 0 && bypassIfNoKeys) {
return next();
}

if (!auth.elevated) {
return sendError(res, 'elevated-claim-required');
}

return next();
};
}
2 changes: 1 addition & 1 deletion src/routes/mfa/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const router = Router();
*/
router.get(
'/mfa/totp/generate',
authenticationGate,
authenticationGate(false),
aw(mfatotpGenerateHandler)
);

Expand Down
8 changes: 7 additions & 1 deletion src/routes/pat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { asyncWrapper as aw } from '@/utils';
import { bodyValidator } from '@/validation';
import { Router } from 'express';
import { createPATHandler, createPATSchema } from './pat';
import { authenticationGate } from '@/middleware/auth';

const router = Router();

Expand All @@ -14,7 +15,12 @@ const router = Router();
* @return {UnauthorizedError} 401 - Unauthenticated user or invalid token - application/json
* @tags General
*/
router.post('/pat', bodyValidator(createPATSchema), aw(createPATHandler));
router.post(
'/pat',
authenticationGate(true),
bodyValidator(createPATSchema),
aw(createPATHandler),
);

const patRouter = router;
export { patRouter };
4 changes: 0 additions & 4 deletions src/routes/pat/pat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ export const createPATHandler: RequestHandler<
{},
{ metadata: object; expiresAt: Date }
> = async (req, res) => {
if (!req.auth) {
return sendError(res, 'unauthenticated-user');
}

try {
const { userId } = req.auth as RequestAuth;

Expand Down
15 changes: 10 additions & 5 deletions src/routes/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const router = Router();
* @security BearerAuth
* @tags User management
*/
router.get('/user', authenticationGate, aw(userHandler));
router.get('/user', authenticationGate(false), aw(userHandler));

/**
* POST /user/password/reset
Expand Down Expand Up @@ -66,6 +66,7 @@ router.post(
*/
router.post(
'/user/password',
authenticationGate(true),
bodyValidator(userPasswordSchema),
aw(userPasswordHandler)
);
Expand Down Expand Up @@ -96,8 +97,8 @@ router.post(
*/
router.post(
'/user/email/change',
authenticationGate(true),
bodyValidator(userEmailChangeSchema),
authenticationGate,
aw(userEmailChange)
);

Expand All @@ -113,8 +114,8 @@ router.post(
*/
router.post(
'/user/mfa',
authenticationGate(true),
bodyValidator(userMfaSchema),
authenticationGate,
aw(userMFAHandler)
);

Expand All @@ -130,8 +131,8 @@ router.post(
*/
router.post(
'/user/deanonymize',
authenticationGate(false),
bodyValidator(userDeanonymizeSchema),
authenticationGate,
aw(userDeanonymizeHandler)
);

Expand Down Expand Up @@ -161,7 +162,11 @@ router.post(
* @return {DisabledEndpointError} 404 - The feature is not activated - application/json
* @tags User management
*/
router.post('/user/webauthn/add', aw(addSecurityKeyHandler));
router.post(
'/user/webauthn/add',
authenticationGate(true, true),
aw(addSecurityKeyHandler),
);

// TODO add @return payload on success
/**
Expand Down
4 changes: 4 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ export const ENV = {
return castBooleanEnv('AUTH_DISABLE_SIGNUP', false);
},

get AUTH_REQUIRE_ELEVATED_CLAIM() {
return castStringEnv('AUTH_REQUIRE_ELEVATED_CLAIM', 'disabled');
}

// * See ../server.ts
// get AUTH_SKIP_INIT() {
// return castBooleanEnv('AUTH_SKIP_INIT', false);
Expand Down
1 change: 1 addition & 0 deletions types/express-request.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface RequestAuth {
userId: string;
defaultRole: string;
isAnonymous: boolean;
elevated: boolean;
}

declare namespace Express {
Expand Down

0 comments on commit 19e7582

Please sign in to comment.