mirror of
https://github.com/zhigang1992/hasura-backend-plus.git
synced 2026-04-30 04:55:14 +08:00
refactor: split config into logical blocks, and rename env vars for more significance (#178)
close #160
This commit is contained in:
12
.env.example
12
.env.example
@@ -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=
|
||||
|
||||
26
.env.test
26
.env.test
@@ -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
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
31
run_tests.sh
31
run_tests.sh
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
12
src/shared/config/authentication/index.ts
Normal file
12
src/shared/config/authentication/index.ts
Normal 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
|
||||
11
src/shared/config/authentication/jwt.ts
Normal file
11
src/shared/config/authentication/jwt.ts
Normal 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')
|
||||
6
src/shared/config/authentication/mfa.ts
Normal file
6
src/shared/config/authentication/mfa.ts
Normal 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)
|
||||
70
src/shared/config/authentication/providers.ts
Normal file
70
src/shared/config/authentication/providers.ts
Normal 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
|
||||
10
src/shared/config/authentication/registration.ts
Normal file
10
src/shared/config/authentication/registration.ts
Normal 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)
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]: [
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user