refactor: split config into logical blocks, and rename env vars for more significance (#178)

close #160
This commit is contained in:
Pilou
2020-04-19 21:23:18 +02:00
committed by GitHub
parent 94d9a55e5f
commit 0c6e38d0d3
29 changed files with 202 additions and 254 deletions

View File

@@ -14,27 +14,27 @@ HASURA_ENDPOINT=
HASURA_GRAPHQL_ADMIN_SECRET=
# Your Hasura JWT secret key. Should be a string value, not JSON (optional)
# JWT_SECRET_KEY=
# JWT_KEY=
# Check passwords against HIBP (optional)
# AUTH_HIBP_ENABLE=false
# HIBP_ENABLE=false
# A string or array used for signing cookies (optional)
# COOKIE_SECRET=
# Recommended defaults:
# AUTH_OTP_ISSUER=HBP
# OTP_ISSUER=HBP
# SERVER_PORT=3000
# AUTH_AUTO_ACTIVATE_NEW_USERS=false
# AUTH_DEFAULT_ROLE=user
# AUTO_ACTIVATE_NEW_USERS=false
# DEFAULT_USER_ROLE=user
# Possible values: RS256, RS384, RS512, HS256, HS384, HS512
# JWT_ALGORITHM=RS256
# JWT_EXPIRES_IN=15
# REFRESH_EXPIRES_IN=43200
# JWT_REFRESH_EXPIRES_IN=43200
# Comma-separated list of allowed email addresses, e.g. 'hasura.io,gmail.com'
# ALLOWED_EMAIL_DOMAINS=

View File

@@ -8,31 +8,31 @@ REDIRECT_URL_SUCCESS=http://frontend.login.com
JWT_ALGORITHM=RS256
AUTH_CLAIMS_FIELDS=name
AUTH_REGISTRATION_FIELDS=name
JWT_CUSTOM_FIELDS=name
REGISTRATION_CUSTOM_FIELDS=name
AUTH_HIBP_ENABLE=true
HIBP_ENABLE=true
# A string or array used for signing cookies (optional)
COOKIE_SECRET=a_cookie_secret_1234
# SERVER_PORT=3000
AUTH_AUTO_ACTIVATE_NEW_USERS=false
AUTO_ACTIVATE_NEW_USERS=false
# OAuth providers
PROVIDERS_SUCCESS_REDIRECT=http://localhost:3001
PROVIDERS_FAILURE_REDIRECT=http://localhost:3001
PROVIDER_SUCCESS_REDIRECT=http://localhost:3001
PROVIDER_FAILURE_REDIRECT=http://localhost:3001
# GitHub
AUTH_GITHUB_ENABLE=true
AUTH_GITHUB_CLIENT_ID=123-456-789
AUTH_GITHUB_CLIENT_SECRET=shhh-its-a-secret
GITHUB_ENABLE=true
GITHUB_CLIENT_ID=123-456-789
GITHUB_CLIENT_SECRET=shhh-its-a-secret
# Google
AUTH_GOOGLE_ENABLE=true
AUTH_GOOGLE_CLIENT_ID=123-456-789
AUTH_GOOGLE_CLIENT_SECRET=shhh-its-a-secret
GOOGLE_ENABLE=true
GOOGLE_CLIENT_ID=123-456-789
GOOGLE_CLIENT_SECRET=shhh-its-a-secret
# JWT_EXPIRES_IN=15
# REFRESH_EXPIRES_IN=43200
# JWT_REFRESH_EXPIRES_IN=43200
# MAX_REQUESTS=100
# TIME_FRAME=15 * 60 * 1000

View File

@@ -15,7 +15,7 @@ on:
env:
HASURA_GRAPHQL_ADMIN_SECRET: test_secret_key
JWT_ALGORITHM: HS256
JWT_SECRET_KEY: never_use_this_secret_key_in_production_this_is_only_for_CI_testing_098hu32r4389ufb4n38994321
JWT_KEY: never_use_this_secret_key_in_production_this_is_only_for_CI_testing_098hu32r4389ufb4n38994321
POSTGRES_PASSWORD: postgrespassword
S3_BUCKET: test-bucket
S3_ACCESS_KEY_ID: 'minio_access_key'
@@ -35,7 +35,7 @@ jobs:
env:
HASURA_GRAPHQL_ADMIN_SECRET: ${{ env.HASURA_GRAPHQL_ADMIN_SECRET }}
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:${{ env.POSTGRES_PASSWORD }}@postgres:5432/postgres
HASURA_GRAPHQL_JWT_SECRET: '{"type": "${{ env.JWT_ALGORITHM }}", "key": "${{ env.JWT_SECRET_KEY }}"}'
HASURA_GRAPHQL_JWT_SECRET: '{"type": "${{ env.JWT_ALGORITHM }}", "key": "${{ env.JWT_KEY }}"}'
options: >-
--restart always
--health-cmd "printf 'GET /healthz HTTP/1.1\r\nHost: graphql-engine\r\n\n' | nc -z graphql-engine 8080 > /dev/null 2>&1 || exit 1"
@@ -58,7 +58,7 @@ jobs:
HASURA_GRAPHQL_ADMIN_SECRET: ${{ env.HASURA_GRAPHQL_ADMIN_SECRET }}
HASURA_ENDPOINT: http://graphql-engine:8080/v1/graphql
JWT_ALGORITHM: ${{ env.JWT_ALGORITHM }}
JWT_SECRET_KEY: ${{ env.JWT_SECRET_KEY }}
JWT_KEY: ${{ env.JWT_KEY }}
S3_ENDPOINT: http://minio:9000
S3_BUCKET: ${{ env.S3_BUCKET }}
S3_ACCESS_KEY_ID: ${{ env.S3_ACCESS_KEY_ID }}

View File

@@ -1,22 +0,0 @@
version: '3.6'
services:
hasura-backend-plus:
build:
context: .
args:
- NODE_ENV=test
env_file: .env.test
environment:
SMTP_HOST: mailhog
SMTP_PORT: 1025
SMTP_SECURE: 'false'
volumes:
- ./coverage:/app/coverage
graphql-engine:
volumes:
- ./mock-config/migrations/1585679214182_custom_user_column:/hasura-migrations/1585679214182_custom_user_column
mailhog:
image: mailhog/mailhog
ports:
- 1025:1025 # smtp server
- 8025:8025 # web ui

View File

@@ -29,30 +29,30 @@
### Authentication
| Name | Default | Description |
| ------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALLOWED_EMAIL_DOMAINS` | | List of comma-separated email domain names that are allowed to register. |
| `AUTH_AUTO_ACTIVATE_NEW_USERS` | false | When set to true, automatically activate the users once registererd. |
| `COOKIE_SECRET` | | |
| `AUTH_DEFAULT_ROLE` | user | |
| `AUTH_HIBP_ENABLE` | false | |
| `JWT_ALGORITHM` | RS256 | Valid values: RS256, RS384, RS512, HS256, HS384, HS512 |
| `JWT_SECRET_KEY` | | Encryption secret. Required when using a SHA (RS*) algorithm. When using a RSA algorithm (RS*), should contain a valid RSA PEM key, otherwise `AUTH_KEY_FILE_PATH` will be used. |
| `JWT_EXPIRES_IN` | 15 | |
| `AUTH_KEY_FILE_PATH` | custom/keys/private.pem | Path to the RSA PEM private key file when using a RSA (RS\*) algorithm and no `JWT_SECRET_KEY` is set. When used, will create a random key if the file is not found. |
| `MIN_PASSWORD_LENGTH` | 3 | Minimum allowed password length. |
| `REDIRECT_URL_ERROR` | | |
| `REDIRECT_URL_SUCCESS` | | |
| `REFRESH_EXPIRES_IN` | 43200 | |
| `SMTP_ENABLE` | false | When set to true, emails are sent on certain steps, like after registration for account activation when autoactivation is deactivated, or for changing emails or passwords |
| `SMTP_HOST` | | SMTP server path to use for sending emails. |
| `SMTP_PASS` | | Password to authenticate on the SMTP server. |
| `SMTP_USER` | | Username to authenticate on the SMTP server. |
| `SMTP_PORT` | 587 | SMTP server port. |
| `SMTP_SECURE` | false | Set to true when the SMTP uses SSL. |
| `AUTH_REGISTRATION_FIELDS` | | Fields that need to be passed on to the registration patload, and that correspond to columns of the `public.users`table. |
| `AUTH_CLAIMS_FIELDS` | | List of comma-separated column names from the `public.users` tables that will be added to the `https://hasura.io/jwt/claims`JWT claims. Column names are kebab-cased and prefixed with `x-`. |
| `AUTH_OTP_ISSUER` | HBP | One-Time Password issuer name used with Muti-factor authentication. |
| Name | Default | Description |
| ---------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALLOWED_EMAIL_DOMAINS` | | List of comma-separated email domain names that are allowed to register. |
| `AUTO_ACTIVATE_NEW_USERS` | false | When set to true, automatically activate the users once registererd. |
| `COOKIE_SECRET` | | |
| `DEFAULT_USER_ROLE` | user | |
| `HIBP_ENABLE` | false | |
| `JWT_ALGORITHM` | RS256 | Valid values: RS256, RS384, RS512, HS256, HS384, HS512 |
| `JWT_KEY` | | Encryption secret. Required when using a SHA (RS*) algorithm. When using a RSA algorithm (RS*), should contain a valid RSA PEM key, otherwise `JWT_KEY_FILE_PATH` will be used. |
| `JWT_EXPIRES_IN` | 15 | |
| `JWT_KEY_FILE_PATH` | custom/keys/private.pem | Path to the RSA PEM private key file when using a RSA (RS\*) algorithm and no `JWT_KEY` is set. When used, will create a random key if the file is not found. |
| `MIN_PASSWORD_LENGTH` | 3 | Minimum allowed password length. |
| `REDIRECT_URL_ERROR` | | |
| `REDIRECT_URL_SUCCESS` | | |
| `JWT_REFRESH_EXPIRES_IN` | 43200 | |
| `SMTP_ENABLE` | false | When set to true, emails are sent on certain steps, like after registration for account activation when autoactivation is deactivated, or for changing emails or passwords |
| `SMTP_HOST` | | SMTP server path to use for sending emails. |
| `SMTP_PASS` | | Password to authenticate on the SMTP server. |
| `SMTP_USER` | | Username to authenticate on the SMTP server. |
| `SMTP_PORT` | 587 | SMTP server port. |
| `SMTP_SECURE` | false | Set to true when the SMTP uses SSL. |
| `REGISTRATION_CUSTOM_FIELDS` | | Fields that need to be passed on to the registration patload, and that correspond to columns of the `public.users`table. |
| `JWT_CUSTOM_FIELDS` | | List of comma-separated column names from the `public.users` tables that will be added to the `https://hasura.io/jwt/claims`JWT claims. Column names are kebab-cased and prefixed with `x-`. |
| `OTP_ISSUER` | HBP | One-Time Password issuer name used with Muti-factor authentication. |
### Storage

View File

@@ -1,31 +0,0 @@
#!/bin/bash
set -e
source bash-utils.sh
script_args $@
export-dotenv .env.test HASURA_GRAPHQL_ADMIN_SECRET
# Load the variables required for the Minio service
export-dotenv .env.test S3_BUCKET
export-dotenv .env.test S3_ACCESS_KEY_ID
export-dotenv .env.test S3_SECRET_ACCESS_KEY
# Create docker services
docker-compose -p hbp_test -f docker-compose.yaml -f docker-compose.test.yaml up -d $build
{ # 'try' block
wait-for http://localhost:3000/healthz "Hasura Backend Plus"
wait-for http://localhost:8080/healthz "Hasura Graphql Engine"
docker exec -i hbp_test_hasura-backend-plus_1 yarn test
} || { # 'catch' block
test_failed=true
}
# 'finally' block
# Remove docker services
docker-compose -p hbp_test down -v --remove-orphans
if [ "$test_failed" = true ] ; then
echo 'Tests failed'
exit 1
fi

View File

@@ -1,11 +1,11 @@
import { AUTH_AUTO_ACTIVATE_NEW_USERS } from '@shared/config'
import { AUTO_ACTIVATE_NEW_USERS } from '@shared/config'
import { Router } from 'express'
import activateAccount from './activate'
import deleteAccount from './delete'
const router = Router()
if (!AUTH_AUTO_ACTIVATE_NEW_USERS) {
if (!AUTO_ACTIVATE_NEW_USERS) {
router.get('/activate', activateAccount)
}

View File

@@ -2,12 +2,7 @@
import 'jest-extended'
import {
AUTH_AUTO_ACTIVATE_NEW_USERS,
AUTH_HIBP_ENABLE,
SERVER_URL,
SMTP_ENABLE
} from '@shared/config'
import { AUTO_ACTIVATE_NEW_USERS, HIBP_ENABLE, SERVER_URL, SMTP_ENABLE } from '@shared/config'
import { HasuraAccountData, generateRandomString } from '@shared/helpers'
import { deleteMailHogEmail, mailHogSearch } from '@shared/test-utils'
@@ -53,7 +48,7 @@ it('should tell the account already exists', async () => {
/**
* * Only run this test if auto activation is disabled
*/
const manualActivationIt = !AUTH_AUTO_ACTIVATE_NEW_USERS ? it : it.skip
const manualActivationIt = !AUTO_ACTIVATE_NEW_USERS ? it : it.skip
manualActivationIt('should activate the account', async () => {
let activateLink: string
if (SMTP_ENABLE) {
@@ -100,7 +95,7 @@ it('should delete the account', async () => {
expect(status).toEqual(204)
})
const pwndPasswordIt = AUTH_HIBP_ENABLE ? it : it.skip
const pwndPasswordIt = HIBP_ENABLE ? it : it.skip
pwndPasswordIt('should tell the password has been pwned', async () => {
const {
status,

View File

@@ -1,4 +1,4 @@
import { AUTH_MFA_ENABLE } from '@shared/config'
import { MFA_ENABLE } from '@shared/config'
import { Router } from 'express'
import account from './account'
import email from './email'
@@ -16,7 +16,7 @@ if (providers) {
router.use('/providers', providers)
}
if (AUTH_MFA_ENABLE) {
if (MFA_ENABLE) {
router.use('/mfa', mfa)
}

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import { asyncWrapper, createQR } from '@shared/helpers'
import { AUTH_OTP_ISSUER } from '@shared/config'
import { OTP_ISSUER } from '@shared/config'
import Boom from '@hapi/boom'
import { authenticator } from 'otplib'
import { request } from '@shared/request'
@@ -18,7 +18,7 @@ async function generateMfa({ headers }: Request, res: Response): Promise<unknown
* Generate OTP secret and key URI.
*/
const otp_secret = authenticator.generateSecret()
const otpAuth = authenticator.keyuri(user_id, AUTH_OTP_ISSUER, otp_secret)
const otpAuth = authenticator.keyuri(user_id, OTP_ISSUER, otp_secret)
try {
await request(updateOtpSecret, { user_id, otp_secret })

View File

@@ -1,7 +1,7 @@
import { Router } from 'express'
import { Strategy, Profile } from '@nicokaiser/passport-apple'
import Boom from '@hapi/boom'
import { AUTH_PROVIDERS } from '@shared/config'
import { PROVIDERS } from '@shared/config'
import { initProvider, UserData } from './utils'
const transformProfile = ({ id, name, email, photos }: Profile): UserData => ({
@@ -12,7 +12,7 @@ const transformProfile = ({ id, name, email, photos }: Profile): UserData => ({
})
export default (router: Router): void => {
const options = AUTH_PROVIDERS.apple
const options = PROVIDERS.apple
// Checks if the strategy is enabled. Don't create any route otherwise
if (options) {

View File

@@ -1,11 +1,11 @@
import { Router } from 'express'
import { Strategy } from 'passport-github2'
import Boom from '@hapi/boom'
import { AUTH_PROVIDERS } from '@shared/config'
import { PROVIDERS } from '@shared/config'
import { initProvider } from './utils'
export default (router: Router): void => {
const options = AUTH_PROVIDERS.github
const options = PROVIDERS.github
// Checks if the strategy is enabled. Don't create any route otherwise
if (options) {
// Checks if the strategy has at least a client ID and a client secret

View File

@@ -2,10 +2,10 @@ import { Router } from 'express'
import { Strategy } from 'passport-google-oauth20'
import Boom from '@hapi/boom'
import { initProvider } from './utils'
import { AUTH_PROVIDERS } from '@shared/config'
import { PROVIDERS } from '@shared/config'
export default (router: Router): void => {
const options = AUTH_PROVIDERS.google
const options = PROVIDERS.google
// Checks if the strategy is enabled. Don't create any route otherwise
if (options) {
// Checks if the strategy has at least a client ID and a client secret

View File

@@ -2,10 +2,10 @@ import { Router } from 'express'
import { Strategy } from 'passport-linkedin-oauth2'
import Boom from '@hapi/boom'
import { initProvider } from './utils'
import { AUTH_PROVIDERS } from '@shared/config'
import { PROVIDERS } from '@shared/config'
export default (router: Router): void => {
const options = AUTH_PROVIDERS.linkedin
const options = PROVIDERS.linkedin
// Checks if the strategy is enabled. Don't create any route otherwise
if (options) {

View File

@@ -2,7 +2,7 @@ import 'jest-extended'
import { initAgent } from '@shared/test-utils'
test('Oauth routes should not exist when disabled', async () => {
const agent = initAgent({ AUTH_PROVIDERS: {} })
const agent = initAgent({ PROVIDERS: {} })
const github = await agent.get('/auth/providers/github')
expect(github.status).toEqual(404)
const google = await agent.get('/auth/providers/google')
@@ -10,7 +10,7 @@ test('Oauth routes should not exist when disabled', async () => {
})
test('Github Oauth should be configured correctly', async () => {
expect(() => initAgent({ AUTH_PROVIDERS: { github: { clientSecret: undefined } } })).toThrow(
expect(() => initAgent({ PROVIDERS: { github: { clientSecret: undefined } } })).toThrow(
'Missing environment variables for GitHub OAuth.'
)
})

View File

@@ -2,11 +2,11 @@ import { Router } from 'express'
import { Strategy } from 'passport-twitter'
import Boom from '@hapi/boom'
import { initProvider } from './utils'
import { AUTH_PROVIDERS, COOKIE_SECRET } from '@shared/config'
import { PROVIDERS, COOKIE_SECRET } from '@shared/config'
import session from 'express-session'
export default (router: Router): void => {
const options = AUTH_PROVIDERS.twitter
const options = PROVIDERS.twitter
// Checks if the strategy is enabled. Don't create any route otherwise
if (options) {

View File

@@ -5,10 +5,10 @@ import { Strategy } from 'passport'
import Boom from '@hapi/boom'
import {
PROVIDERS_SUCCESS_REDIRECT,
PROVIDERS_FAILURE_REDIRECT,
PROVIDER_SUCCESS_REDIRECT,
PROVIDER_FAILURE_REDIRECT,
SERVER_URL,
AUTH_PROVIDERS
PROVIDERS
} from '@shared/config'
import { insertAccount, selectAccountProvider } from '@shared/queries'
import { request } from '@shared/request'
@@ -44,7 +44,7 @@ const manageProviderStrategy = (
profile: Profile,
done: VerifyCallback
): Promise<void> => {
// TODO How do we handle AUTH_REGISTRATION_FIELDS with OAuth?
// TODO How do we handle REGISTRATION_CUSTOM_FIELDS with OAuth?
// find or create the user
// check if user exists, using profile.id
const { id, email, display_name, avatar_url } = transformProfile(profile)
@@ -100,7 +100,7 @@ const providerCallback = async (req: Request, res: Response): Promise<void> => {
await setRefreshToken(res, account.id)
// redirect back user to app url
res.redirect(PROVIDERS_SUCCESS_REDIRECT as string)
res.redirect(PROVIDER_SUCCESS_REDIRECT as string)
}
export const initProvider = <T extends Strategy>(
@@ -122,7 +122,7 @@ export const initProvider = <T extends Strategy>(
passport.use(
new strategy(
{
...AUTH_PROVIDERS[strategyName],
...PROVIDERS[strategyName],
...options,
callbackURL: `${SERVER_URL}/auth/providers/${strategyName}/callback`,
passReqToCallback: true
@@ -137,7 +137,7 @@ export const initProvider = <T extends Strategy>(
const handlers = [
passport.authenticate(strategyName, {
failureRedirect: PROVIDERS_FAILURE_REDIRECT,
failureRedirect: PROVIDER_FAILURE_REDIRECT,
session: false
}),
providerCallback

View File

@@ -1,4 +1,4 @@
import { AUTH_AUTO_ACTIVATE_NEW_USERS, SERVER_URL, SMTP_ENABLE } from '@shared/config'
import { AUTO_ACTIVATE_NEW_USERS, SERVER_URL, SMTP_ENABLE } from '@shared/config'
import { Request, Response } from 'express'
import { asyncWrapper, checkHibp, hashPassword, selectAccount } from '@shared/helpers'
@@ -37,7 +37,7 @@ async function registerAccount({ body }: Request, res: Response): Promise<unknow
throw Boom.badImplementation()
}
if (!AUTH_AUTO_ACTIVATE_NEW_USERS && SMTP_ENABLE) {
if (!AUTO_ACTIVATE_NEW_USERS && SMTP_ENABLE) {
try {
await emailClient.send({
template: 'confirm',

View File

@@ -1,95 +0,0 @@
import Boom from '@hapi/boom'
import path from 'path'
import { castBooleanEnv, castStringArrayEnv, castIntEnv } from './utils'
import { REDIRECT_URL_SUCCESS, REDIRECT_URL_ERROR } from './application'
/**
* * Authentication settings
*/
export const {
COOKIE_SECRET,
JWT_SECRET_KEY,
AUTH_OTP_ISSUER = 'HBP',
ALLOWED_EMAIL_DOMAINS,
JWT_ALGORITHM = 'RS256',
AUTH_DEFAULT_ROLE = 'user'
} = process.env
export const AUTH_ENABLE = castBooleanEnv('AUTH_ENABLE', true)
export const AUTH_AUTO_ACTIVATE_NEW_USERS = castBooleanEnv('AUTH_AUTO_ACTIVATE_NEW_USERS')
export const AUTH_CLAIMS_FIELDS = castStringArrayEnv('AUTH_CLAIMS_FIELDS')
export const AUTH_HIBP_ENABLE = castBooleanEnv('AUTH_HIBP_ENABLE')
export const AUTH_KEY_FILE_PATH = path.resolve(process.env.PWD || '.', 'custom/keys/private.pem')
export const AUTH_REGISTRATION_FIELDS = castStringArrayEnv('AUTH_REGISTRATION_FIELDS')
export const REFRESH_EXPIRES_IN = castIntEnv('REFRESH_EXPIRES_IN', 43200)
export const JWT_EXPIRES_IN = castIntEnv('JWT_EXPIRES_IN', 15)
export const MIN_PASSWORD_LENGTH = castIntEnv('MIN_PASSWORD_LENGTH', 3)
// Multi-Factor Authentication configuration
export const AUTH_MFA_ENABLE = castBooleanEnv('AUTH_MFA_ENABLE', true)
/**
* * OAuth settings
*/
export const {
// External OAuth provider redirect URLS
PROVIDERS_SUCCESS_REDIRECT = REDIRECT_URL_SUCCESS,
PROVIDERS_FAILURE_REDIRECT = REDIRECT_URL_ERROR
} = process.env
const AUTH_PROVIDERS: Record<string, Record<string, string | undefined>> = {}
// Github OAuth2 provider settings
if (castBooleanEnv('AUTH_GITHUB_ENABLE')) {
AUTH_PROVIDERS.github = {
clientID: process.env.AUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET,
authorizationURL: process.env.AUTH_GITHUB_AUTHORIZATION_URL, // optional
tokenURL: process.env.AUTH_GITHUB_TOKEN_URL, // optional
userProfileURL: process.env.AUTH_GITHUB_USER_PROFILE_URL // optional
}
}
// Google OAuth2 provider settings
if (castBooleanEnv('AUTH_GOOGLE_ENABLE')) {
AUTH_PROVIDERS.google = {
clientID: process.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET
}
}
// Twitter provider settings
if (castBooleanEnv('AUTH_TWITTER_ENABLE')) {
AUTH_PROVIDERS.twitter = {
consumerKey: process.env.AUTH_TWITTER_CONSUMER_KEY,
consumerSecret: process.env.AUTH_TWITTER_CONSUMER_SECRET
}
}
// LinkedIn OAuth2 provider settings
if (castBooleanEnv('AUTH_LINKEDIN_ENABLE')) {
AUTH_PROVIDERS.linkedin = {
clientID: process.env.AUTH_LINKEDIN_CLIENT_ID,
clientSecret: process.env.AUTH_LINKEDIN_CLIENT_SECRET
}
}
// Apple OAuth2 provider settings
if (castBooleanEnv('AUTH_APPLE_ENABLE')) {
try {
AUTH_PROVIDERS.apple = {
clientID: process.env.AUTH_APPLE_CLIENT_ID,
teamID: process.env.AUTH_APPLE_TEAM_ID,
keyID: process.env.AUTH_APPLE_KEY_ID,
key:
process.env.AUTH_APPLE_PRIVATE_KEY &&
// Convert contents from base64 string to string to avoid issues with line breaks in the environment variable
Buffer.from(process.env.AUTH_APPLE_PRIVATE_KEY, 'base64').toString('ascii')
}
} catch (e) {
throw Boom.badImplementation(`Invalid Apple OAuth Key file.`)
}
}
export { AUTH_PROVIDERS }
// True if at least one of the providers is enabled
export const AUTH_HAS_ONE_PROVIDER = !!Object.keys(AUTH_PROVIDERS).length

View File

@@ -0,0 +1,12 @@
import { castBooleanEnv } from '../utils'
export * from './registration'
export * from './jwt'
export * from './providers'
export * from './mfa'
/**
* * Authentication settings
*/
export const AUTH_ENABLE = castBooleanEnv('AUTH_ENABLE', true)
export const { COOKIE_SECRET } = process.env

View File

@@ -0,0 +1,11 @@
import path from 'path'
import { castIntEnv, castStringArrayEnv } from '../utils'
/**
* * Authentication settings
*/
export const { JWT_KEY, JWT_ALGORITHM = 'RS256' } = process.env
export const JWT_KEY_FILE_PATH = path.resolve(process.env.PWD || '.', 'custom/keys/private.pem')
export const JWT_EXPIRES_IN = castIntEnv('JWT_EXPIRES_IN', 15)
export const JWT_REFRESH_EXPIRES_IN = castIntEnv('JWT_REFRESH_EXPIRES_IN', 43200)
export const JWT_CUSTOM_FIELDS = castStringArrayEnv('JWT_CUSTOM_FIELDS')

View File

@@ -0,0 +1,6 @@
import { castBooleanEnv } from '../utils'
export const { OTP_ISSUER = 'HBP' } = process.env
// Multi-Factor Authentication configuration
export const MFA_ENABLE = castBooleanEnv('MFA_ENABLE', true)

View File

@@ -0,0 +1,70 @@
import Boom from '@hapi/boom'
import { castBooleanEnv } from '../utils'
import { REDIRECT_URL_SUCCESS, REDIRECT_URL_ERROR } from '../application'
/**
* * OAuth settings
*/
export const {
// External OAuth provider redirect URLS
PROVIDER_SUCCESS_REDIRECT = REDIRECT_URL_SUCCESS,
PROVIDER_FAILURE_REDIRECT = REDIRECT_URL_ERROR
} = process.env
const PROVIDERS: Record<string, Record<string, string | undefined>> = {}
// Github OAuth2 provider settings
if (castBooleanEnv('GITHUB_ENABLE')) {
PROVIDERS.github = {
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
authorizationURL: process.env.GITHUB_AUTHORIZATION_URL, // optional
tokenURL: process.env.GITHUB_TOKEN_URL, // optional
userProfileURL: process.env.GITHUB_USER_PROFILE_URL // optional
}
}
// Google OAuth2 provider settings
if (castBooleanEnv('GOOGLE_ENABLE')) {
PROVIDERS.google = {
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
}
}
// Twitter provider settings
if (castBooleanEnv('TWITTER_ENABLE')) {
PROVIDERS.twitter = {
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET
}
}
// LinkedIn OAuth2 provider settings
if (castBooleanEnv('LINKEDIN_ENABLE')) {
PROVIDERS.linkedin = {
clientID: process.env.LINKEDIN_CLIENT_ID,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET
}
}
// Apple OAuth2 provider settings
if (castBooleanEnv('APPLE_ENABLE')) {
try {
PROVIDERS.apple = {
clientID: process.env.APPLE_CLIENT_ID,
teamID: process.env.APPLE_TEAM_ID,
keyID: process.env.APPLE_KEY_ID,
key:
process.env.APPLE_PRIVATE_KEY &&
// Convert contents from base64 string to string to avoid issues with line breaks in the environment variable
Buffer.from(process.env.APPLE_PRIVATE_KEY, 'base64').toString('ascii')
}
} catch (e) {
throw Boom.badImplementation(`Invalid Apple OAuth Key file.`)
}
}
export { PROVIDERS }
// True if at least one of the providers is enabled
export const AUTH_HAS_ONE_PROVIDER = !!Object.keys(PROVIDERS).length

View File

@@ -0,0 +1,10 @@
import { castBooleanEnv, castStringArrayEnv, castIntEnv } from '../utils'
/**
* * Registration settings
* */
export const { ALLOWED_EMAIL_DOMAINS, DEFAULT_USER_ROLE = 'user' } = process.env
export const AUTO_ACTIVATE_NEW_USERS = castBooleanEnv('AUTO_ACTIVATE_NEW_USERS')
export const HIBP_ENABLE = castBooleanEnv('HIBP_ENABLE')
export const REGISTRATION_CUSTOM_FIELDS = castStringArrayEnv('REGISTRATION_CUSTOM_FIELDS')
export const MIN_PASSWORD_LENGTH = castIntEnv('MIN_PASSWORD_LENGTH', 3)

View File

@@ -1,4 +1,4 @@
import { AUTH_DEFAULT_ROLE, AUTH_HIBP_ENABLE, REFRESH_EXPIRES_IN } from './config'
import { DEFAULT_USER_ROLE, HIBP_ENABLE, JWT_REFRESH_EXPIRES_IN } from './config'
import { ClaimValueType, sign } from './jwt'
import { NextFunction, Request, Response } from 'express'
import {
@@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid'
*/
export function newRefreshExpiry(): number {
const now = new Date()
const days = REFRESH_EXPIRES_IN / 1440
const days = JWT_REFRESH_EXPIRES_IN / 1440
return now.setDate(now.getDate() + days)
}
@@ -32,7 +32,7 @@ export function newRefreshExpiry(): number {
* @param roles Defaults to ["user"].
*/
export function createHasuraJwt({
default_role = AUTH_DEFAULT_ROLE,
default_role = DEFAULT_USER_ROLE,
account_roles = [],
user
}: AccountData): string {
@@ -164,7 +164,7 @@ export const hashPassword = async (password: string): Promise<string> => {
* @param password Password to check.
*/
export const checkHibp = async (password: string): Promise<void> => {
if (AUTH_HIBP_ENABLE && (await pwnedPassword(password))) {
if (HIBP_ENABLE && (await pwnedPassword(password))) {
throw Boom.badRequest('Password is too weak.')
}
}

View File

@@ -1,10 +1,4 @@
import {
AUTH_KEY_FILE_PATH,
COOKIE_SECRET,
JWT_ALGORITHM,
JWT_EXPIRES_IN,
JWT_SECRET_KEY
} from './config'
import { JWT_KEY_FILE_PATH, COOKIE_SECRET, JWT_ALGORITHM, JWT_EXPIRES_IN, JWT_KEY } from './config'
import { JWK, JWKS, JWT } from 'jose'
import Boom from '@hapi/boom'
@@ -18,13 +12,13 @@ import { v4 as uuidv4 } from 'uuid'
const RSA_TYPES = ['RS256', 'RS384', 'RS512']
const SHA_TYPES = ['HS256', 'HS384', 'HS512']
let jwtKey: string | JWK.RSAKey | JWK.ECKey | JWK.OKPKey | JWK.OctKey = JWT_SECRET_KEY as string
let jwtKey: string | JWK.RSAKey | JWK.ECKey | JWK.OKPKey | JWK.OctKey = JWT_KEY as string
/**
* * Sets the JWT Key.
* * If RSA algorithm, then checks if the PEM has been passed on through the JWT_SECRET_KEY
* * If RSA algorithm, then checks if the PEM has been passed on through the JWT_KEY
* * If not, tries to read the private.pem file, or generates it otherwise
* * If SHA algorithm, then uses the JWT_SECRET_KEY environment variables
* * If SHA algorithm, then uses the JWT_KEY environment variables
*/
if (RSA_TYPES.includes(JWT_ALGORITHM)) {
if (jwtKey) {
@@ -32,17 +26,15 @@ if (RSA_TYPES.includes(JWT_ALGORITHM)) {
jwtKey = JWK.asKey(jwtKey, { alg: JWT_ALGORITHM })
jwtKey.toPEM(true)
} catch (error) {
throw Boom.badImplementation(
'Invalid RSA private key in the JWT_SECRET_KEY environment variable.'
)
throw Boom.badImplementation('Invalid RSA private key in the JWT_KEY environment variable.')
}
} else {
try {
const file = fs.readFileSync(AUTH_KEY_FILE_PATH)
const file = fs.readFileSync(JWT_KEY_FILE_PATH)
jwtKey = JWK.asKey(file)
} catch (error) {
jwtKey = JWK.generateSync('RSA', 2048, { alg: JWT_ALGORITHM, use: 'sig' }, true)
fs.writeFileSync(AUTH_KEY_FILE_PATH, jwtKey.toPEM(true))
fs.writeFileSync(JWT_KEY_FILE_PATH, jwtKey.toPEM(true))
}
}
} else if (SHA_TYPES.includes(JWT_ALGORITHM)) {

View File

@@ -1,4 +1,4 @@
import { AUTH_CLAIMS_FIELDS } from './config'
import { JWT_CUSTOM_FIELDS } from './config'
import gql from 'graphql-tag'
const accountFragment = gql`
@@ -11,7 +11,7 @@ const accountFragment = gql`
}
user {
id
${AUTH_CLAIMS_FIELDS.join('\n\t\t\t')}
${JWT_CUSTOM_FIELDS.join('\n\t\t\t')}
}
is_anonymous
ticket

View File

@@ -1,4 +1,4 @@
import { ALLOWED_EMAIL_DOMAINS, AUTH_REGISTRATION_FIELDS, MIN_PASSWORD_LENGTH } from './config'
import { ALLOWED_EMAIL_DOMAINS, REGISTRATION_CUSTOM_FIELDS, MIN_PASSWORD_LENGTH } from './config'
import Joi from '@hapi/joi'
interface ExtendedStringSchema extends Joi.StringSchema {
@@ -47,7 +47,7 @@ const accountFields = {
export const userDataFields = {
user_data: Joi.object(
AUTH_REGISTRATION_FIELDS.reduce<{ [k: string]: Joi.Schema[] }>(
REGISTRATION_CUSTOM_FIELDS.reduce<{ [k: string]: Joi.Schema[] }>(
(aggr, key) => ({
...aggr,
[key]: [

View File

@@ -1,7 +1,7 @@
import { SuperTest, Test, agent } from 'supertest'
import { TestAccount, createAccount, deleteEmailsOfAccount } from '@shared/test-utils'
import { AUTH_AUTO_ACTIVATE_NEW_USERS } from '@shared/config'
import { AUTO_ACTIVATE_NEW_USERS } from '@shared/config'
import Boom from '@hapi/boom'
import { HasuraAccountData } from '@shared/helpers'
import { JWT } from 'jose'
@@ -27,7 +27,7 @@ beforeAll(async () => {
// * Create a mock account
const { email, password } = createAccount()
await request.post('/auth/register').send({ email, password })
if (!AUTH_AUTO_ACTIVATE_NEW_USERS) {
if (!AUTO_ACTIVATE_NEW_USERS) {
const hasuraData = (await admin(selectAccountByEmail, { email })) as HasuraAccountData
const ticket = hasuraData.auth_accounts[0].ticket
await request.get(`/auth/account/activate?ticket=${ticket}`)