Skip to content

Commit

Permalink
feat: disable signup (#444)
Browse files Browse the repository at this point in the history
* feat: validate signup disabled during email/password registration

* fix: return forbidden status code for disabled sign up

* feat: check if signup is disabled upon OAuth registration

* chore: remove unnecessary blank line in app.ts

* feat: check if signup is disabled upon passwordless email signup

* feat: check if signup disabled upon passwordless SMS signin

* feat: check if disabled upon signup via webauthn

* chore: add changeset

* chore: update docs about `AUTH_DISABLE_SIGNUP`
  • Loading branch information
onehassan authored Nov 15, 2023
1 parent 15459a9 commit cd8c786
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-shrimps-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hasura-auth': minor
---

feat: add option to disable user sign-up through `AUTH_DISABLE_SIGNUP`
3 changes: 2 additions & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
| HASURA_GRAPHQL_ADMIN_SECRET<b>\*</b> | Hasura GraphQL Admin Secret. Required to manipulate account data. | |
| AUTH_HOST | Server host. This option is available until Hasura-auth `v0.6.0`. [Docs](http://expressjs.com/en/5x/api.html#app.listen) | `0.0.0.0` |
| AUTH_PORT | Server port. [Docs](http://expressjs.com/en/5x/api.html#app.listen) | `4000` |
| AUTH_API_PREFIX | API prefix | `/` |
| AUTH_API_PREFIX | API prefix | `/` |
| AUTH_SERVER_URL | Server URL of where Hasura Backend Plus is running. This value is to used as a callback in email templates and for the OAuth authentication process. | |
| AUTH_CLIENT_URL | URL of your frontend application. Used to redirect users to the right page once actions based on emails or OAuth succeed. | |
| AUTH_CONCEAL_ERRORS | Conceal sensitive error messages to avoid leaking information about user accounts to attackers | `false` |
Expand All @@ -24,6 +24,7 @@
| AUTH_GRAVATAR_RATING | One of 'g', 'pg', 'r', 'x'. | `g` |
| AUTH_ANONYMOUS_USERS_ENABLED | Enables users to register as an anonymous user. | `false` |
| AUTH_DISABLE_NEW_USERS | If set, new users will be disabled after finishing registration and won't be able to connect. | `false` |
| AUTH_DISABLE_SIGNUP | If set to true, all signup methods will throw an unauthorized error. | `false` |
| AUTH_ACCESS_CONTROL_ALLOWED_EMAILS | Comma-separated list of emails that are allowed to register. | |
| AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS | Comma-separated list of email domains that are allowed to register. If `ALLOWED_EMAIL_DOMAINS` is `tesla.com,ikea.se`, only emails from tesla.com and ikea.se would be allowed to register an account. | `` (allow all email domains) |
| AUTH_ACCESS_CONTROL_BLOCKED_EMAILS | Comma-separated list of emails that cannot register. | |
Expand Down
1 change: 0 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { addOpenApiRoute } from './openapi';
import router from './routes';
import { ENV } from './utils/env';


const app = express();

if (process.env.NODE_ENV === 'production') {
Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export const ERRORS = asErrors({
status: StatusCodes.INTERNAL_SERVER_ERROR,
message: 'Invalid OAuth configuration',
},
'signup-disabled': {
status: StatusCodes.FORBIDDEN,
message: 'Sign up is disabled.',
},
});

export const sendError = (
Expand Down
12 changes: 9 additions & 3 deletions src/routes/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,10 @@ export const oauthProviders = Router()
* @see {@link file://./config/index.ts}
*/
.use((req, res, next) => {
res.locals.grant = {dynamic: {
origin: `${req.protocol}://${req.headers.host}`},
res.locals.grant = {
dynamic: {
origin: `${req.protocol}://${req.headers.host}`,
},
};
next();
})
Expand Down Expand Up @@ -274,7 +276,11 @@ export const oauthProviders = Router()
}
} else {
// * No user found with this email. Create a new user
// TODO feature: check if registration is enabled

if (ENV.AUTH_DISABLE_SIGNUP) {
return sendError(res, 'signup-disabled');
}

const userInput = await transformOauthProfile(profile, options);
user = await insertUser({
...userInput,
Expand Down
4 changes: 4 additions & 0 deletions src/routes/signin/passwordless/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const signInPasswordlessEmailHandler: RequestHandler<

// if no user exists, create the user
if (!user) {
if (ENV.AUTH_DISABLE_SIGNUP) {
return sendError(res, 'signup-disabled');
}

user = await insertUser({
displayName: displayName ?? email,
locale,
Expand Down
4 changes: 4 additions & 0 deletions src/routes/signin/passwordless/sms/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export const signInPasswordlessSmsHandler: RequestHandler<

// if no user exists, create the user
if (!userExists) {
if (ENV.AUTH_DISABLE_SIGNUP) {
return sendError(res, 'signup-disabled');
}

user = await insertUser({
disabled: ENV.AUTH_DISABLE_NEW_USERS,
displayName,
Expand Down
4 changes: 4 additions & 0 deletions src/routes/signup/email-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const signUpEmailPasswordHandler: RequestHandler<
const { body } = req;
const { email, password, options } = body;

if (ENV.AUTH_DISABLE_SIGNUP) {
return sendError(res, 'signup-disabled');
}

// check if email already in use by some other user
if (await getUserByEmail(email)) {
return sendError(res, 'email-already-in-use');
Expand Down
4 changes: 4 additions & 0 deletions src/routes/signup/webauthn/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const signUpWebauthnHandler: RequestHandler<
return sendError(res, 'disabled-endpoint');
}

if (ENV.AUTH_DISABLE_SIGNUP) {
return sendError(res, 'signup-disabled');
}

// check if email already in use by some other user
if (await getUserByEmail(email)) {
return sendError(res, 'email-already-in-use');
Expand Down
4 changes: 4 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ export const ENV = {
return castBooleanEnv('AUTH_SHOW_LOG_QUERY_PARAMS', false);
},

get AUTH_DISABLE_SIGNUP() {
return castBooleanEnv('AUTH_DISABLE_SIGNUP', false);
},

// * See ../server.ts
// get AUTH_SKIP_INIT() {
// return castBooleanEnv('AUTH_SKIP_INIT', false);
Expand Down
13 changes: 13 additions & 0 deletions test/routes/signin/passwordless/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,17 @@ describe('passwordless email (magic link)', () => {
error: 'invalid-request',
});
});

it('should not be possible to signin in when signup is disabled', async () => {
await request.post('/change-env').send({
AUTH_DISABLE_SIGNUP: true,
});

await request
.post('/signin/passwordless/email')
.send({
email: faker.internet.email(),
})
.expect(StatusCodes.FORBIDDEN);
});
});
16 changes: 16 additions & 0 deletions test/routes/signin/passwordless/sms/sms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,20 @@ describe('passwordless sms', () => {
})
.expect(StatusCodes.INTERNAL_SERVER_ERROR);
});

it('should fail when signup is disabled', async () => {
const phoneNumber = `+3598${faker.phone.phoneNumber('########')}`;

await request.post('/change-env').send({
AUTH_DISABLE_SIGNUP: true,
AUTH_SMS_TEST_PHONE_NUMBERS: phoneNumber,
});

await request
.post('/signin/passwordless/sms')
.send({
phoneNumber,
})
.expect(StatusCodes.FORBIDDEN);
});
});
13 changes: 13 additions & 0 deletions test/routes/signin/webauthn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,17 @@ describe('webauthn', () => {
.send({ email, credential })
.expect(StatusCodes.BAD_REQUEST);
});

it('should fail if signup is disabled', async () => {
const email = faker.internet.email();

await request.post('/change-env').send({
AUTH_DISABLE_SIGNUP: true,
});

await request
.post('/signup/webauthn')
.send({ email })
.expect(StatusCodes.FORBIDDEN);
});
});
19 changes: 19 additions & 0 deletions test/routes/signup/email-password.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,23 @@ describe('email-password', () => {
'The value of "options.redirectTo" is not allowed.'
);
});

it('should return an unauthorized error when signup is disabled', async () => {
const email = faker.internet.email();
const password = faker.internet.password();

await request.post('/change-env').send({
AUTH_DISABLE_SIGNUP: true,
});

const { body } = await request
.post('/signup/email-password')
.send({
email,
password,
})
.expect(StatusCodes.FORBIDDEN);

expect(body.message).toEqual('Sign up is disabled.');
});
});

0 comments on commit cd8c786

Please sign in to comment.