├── pages ├── iam │ ├── login.vue │ ├── reset.vue │ ├── register.vue │ ├── dashboard.vue │ ├── verifyfailed.vue │ ├── verifysuccessful.vue │ ├── dashboard │ │ ├── index.vue │ │ ├── denied.vue │ │ ├── admin.vue │ │ ├── profile.vue │ │ └── settings.vue │ ├── verify.vue │ ├── verifyemail.vue │ ├── docs │ │ ├── getting-started.vue │ │ ├── index.vue │ │ ├── frontend.vue │ │ ├── concepts.vue │ │ └── features.vue │ └── index.vue ├── sample.vue ├── contact.vue ├── protected.vue └── index.vue ├── .gitignore ├── app.vue ├── iam ├── ui │ └── img │ │ ├── nuxt-iam-logo.png │ │ └── nuxt-iam-logo-symbol.png ├── mvc │ ├── authn │ │ ├── middleware.ts │ │ └── controller.ts │ ├── users │ │ ├── middleware.ts │ │ ├── controller.ts │ │ ├── queries.ts │ │ └── model.ts │ ├── doodads │ │ ├── controller.ts │ │ └── model.ts │ └── refresh-tokens │ │ ├── controller.ts │ │ ├── model.ts │ │ └── queries.ts ├── authz │ ├── permissions.ts │ └── helpers.ts ├── middleware │ └── index.ts └── misc │ ├── types.d.ts │ └── utils │ ├── passwords.ts │ ├── sessions.ts │ └── logins.ts ├── components ├── IamLogo.vue ├── iamVerifyFailed.vue ├── IamLogoLink.vue ├── IamOrSeparator.vue ├── IamFooter.vue ├── iamVerifySuccessful.vue ├── iamVerifyEmailToken.vue ├── iamVerifyPasswordReset.vue ├── NxDropdown.vue ├── NxAvatar.vue ├── NxModal.vue ├── iamReset.vue ├── NxCard.vue ├── NxObjectAsTable.vue ├── NxMenu.vue ├── iamLogin.vue ├── iamRefreshTokensTable.vue ├── iamRegister.vue ├── NxAlert.vue ├── NxForm.vue ├── iamDashboard.vue ├── IamDashboardHeader.vue ├── NxNavbar.vue ├── iamUsersTable.vue └── NxButton.vue ├── tsconfig.json ├── server ├── api │ └── iam │ │ ├── authn │ │ ├── [...].ts │ │ └── index.ts │ │ ├── users │ │ ├── [...].ts │ │ └── index.ts │ │ ├── refresh-tokens │ │ ├── [...].ts │ │ └── index.ts │ │ └── doodads │ │ ├── [...].ts │ │ └── index.ts └── middleware │ └── iam │ ├── logs │ └── requests.ts │ ├── misc │ └── platform.ts │ └── authentication │ └── index.ts ├── plugins └── todays.ts ├── assets └── iam │ └── resources │ └── css │ └── style.css ├── layouts ├── custom.vue └── default.vue ├── error.vue ├── package.json ├── stores └── useIamProfileStore.ts ├── nuxt.config.ts ├── .env.example ├── README.md ├── prisma └── schema.prisma └── composables ├── useIamAdmin.ts └── useIam.ts /pages/iam/login.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /pages/iam/reset.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /pages/iam/register.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /pages/iam/dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /iam/ui/img/nuxt-iam-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycoder/nuxt-iam/HEAD/iam/ui/img/nuxt-iam-logo.png -------------------------------------------------------------------------------- /pages/iam/verifyfailed.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /pages/iam/verifysuccessful.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/IamLogo.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /iam/ui/img/nuxt-iam-logo-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycoder/nuxt-iam/HEAD/iam/ui/img/nuxt-iam-logo-symbol.png -------------------------------------------------------------------------------- /pages/iam/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /pages/iam/verify.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /pages/iam/verifyemail.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /server/api/iam/authn/[...].ts: -------------------------------------------------------------------------------- 1 | import authnController from "~~/iam/mvc/authn/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return authnController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /server/api/iam/authn/index.ts: -------------------------------------------------------------------------------- 1 | import authnController from "~~/iam/mvc/authn/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return authnController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /server/api/iam/users/[...].ts: -------------------------------------------------------------------------------- 1 | import usersController from "~~/iam/mvc/users/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return await usersController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /server/api/iam/users/index.ts: -------------------------------------------------------------------------------- 1 | import usersController from "~~/iam/mvc/users/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return await usersController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /server/middleware/iam/logs/requests.ts: -------------------------------------------------------------------------------- 1 | // Log all requests 2 | export default defineEventHandler(async (event) => { 3 | console.log('request: ', event.node.req.method, ' ', event.node.req.url) 4 | }) -------------------------------------------------------------------------------- /pages/iam/dashboard/denied.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /server/api/iam/refresh-tokens/[...].ts: -------------------------------------------------------------------------------- 1 | import refreshTokensController from "~~/iam/mvc/refresh-tokens/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return refreshTokensController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /server/api/iam/refresh-tokens/index.ts: -------------------------------------------------------------------------------- 1 | import refreshTokensController from "~~/iam/mvc/refresh-tokens/controller"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | return refreshTokensController(event); 5 | }); 6 | -------------------------------------------------------------------------------- /pages/sample.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /plugins/todays.ts: -------------------------------------------------------------------------------- 1 | // This plugin returns today's date 2 | import dayjs from "dayjs"; 3 | 4 | const now = dayjs(); 5 | const today = dayjs(now).format("dddd, DD MMMM, YYYY"); 6 | 7 | export default defineNuxtPlugin(async (nuxtApp) => { 8 | nuxtApp.today = today; 9 | }); 10 | -------------------------------------------------------------------------------- /components/iamVerifyFailed.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/IamLogoLink.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /server/api/iam/doodads/[...].ts: -------------------------------------------------------------------------------- 1 | /** This is an example endpoint and can be deleted. 2 | * If you delete it, also delete ~~iam/mvc/doodads directory 3 | */ 4 | 5 | import doodadsController from "~~/iam/mvc/doodads/controller"; 6 | 7 | export default defineEventHandler(async (event) => { 8 | return doodadsController(event); 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/iam/doodads/index.ts: -------------------------------------------------------------------------------- 1 | /** This is an example endpoint and can be deleted. 2 | * If you delete it, also delete ~~iam/mvc/doodads directory 3 | */ 4 | 5 | import doodadsController from "~~/iam/mvc/doodads/controller"; 6 | 7 | export default defineEventHandler(async (event) => { 8 | return doodadsController(event); 9 | }); 10 | -------------------------------------------------------------------------------- /iam/mvc/authn/middleware.ts: -------------------------------------------------------------------------------- 1 | /** Middleware for all authn routes 2 | * Middleware should only return error or void 3 | */ 4 | import { H3Event, H3Error } from "h3"; 5 | import { getClientPlatform } from "~~/iam/middleware/"; 6 | 7 | export function authnMiddleware(event: H3Event): H3Error | string { 8 | return getClientPlatform(event); 9 | } 10 | -------------------------------------------------------------------------------- /pages/contact.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /components/IamOrSeparator.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /iam/mvc/users/middleware.ts: -------------------------------------------------------------------------------- 1 | /** Middleware for users routes 2 | * Feel free to define other middleware 3 | * Middleware should only return error or void 4 | */ 5 | import { H3Event, H3Error } from "h3"; 6 | import { getClientPlatform } from "~~/iam/middleware/"; 7 | 8 | /** 9 | * @desc Middleware for all user routes 10 | * @param event 11 | * @returns 12 | */ 13 | export function usersMiddleware(event: H3Event): H3Error | string { 14 | return getClientPlatform(event); 15 | } 16 | -------------------------------------------------------------------------------- /components/IamFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /components/iamVerifySuccessful.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /pages/protected.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /assets/iam/resources/css/style.css: -------------------------------------------------------------------------------- 1 | /** GLOBAL STYLES FOR NUXT IAM **/ 2 | 3 | body { 4 | font-family: Helvetica, Arial, sans-serif; 5 | font-size: 1rem; 6 | font-weight: 400; 7 | line-height: 1.5; 8 | color: #212529; 9 | background-color: #fff; 10 | } 11 | 12 | /* Spinner */ 13 | .loading-spinner { 14 | display: inline-block; 15 | width: 80px; 16 | height: 80px; 17 | } 18 | .loading-spinner:after { 19 | content: " "; 20 | display: block; 21 | width: 64px; 22 | height: 64px; 23 | margin: 8px; 24 | border-radius: 50%; 25 | border: 6px solid #fff; 26 | border-color: #184b81 transparent #184b81 transparent; 27 | animation: loading-spinner 1.2s linear infinite; 28 | } 29 | @keyframes loading-spinner { 30 | 0% { 31 | transform: rotate(0deg); 32 | } 33 | 100% { 34 | transform: rotate(360deg); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /layouts/custom.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/middleware/iam/misc/platform.ts: -------------------------------------------------------------------------------- 1 | // Check for client-platform, a required header 2 | 3 | export default defineEventHandler(async (event) => { 4 | 5 | // const clientPlatforms = ["app", "browser", "browser-dev"]; 6 | // let clientPlatform = event.node.req.headers["client-platform"] as string; 7 | 8 | // // Check if 'client-platform' header is present 9 | // if (!clientPlatform) { 10 | // console.log( 11 | // "Missing required header 'client-platform'. 'client-platform' upgraded to 'browser'" 12 | // ); 13 | 14 | // // Add client-platform to request headers 15 | // event.node.req.headers["client-platform"] = "browser"; 16 | // clientPlatform = "browser"; 17 | // } 18 | 19 | // // Check if 'client-platform' in included in valid client platforms 20 | // if (!clientPlatforms.includes(clientPlatform)) 21 | // throw createError({ 22 | // statusCode: 400, 23 | // statusMessage: 24 | // "Required header 'client-platform' must be 'app', 'browser', or 'browser-dev' only", 25 | // }); 26 | }) -------------------------------------------------------------------------------- /components/iamVerifyEmailToken.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | -------------------------------------------------------------------------------- /components/iamVerifyPasswordReset.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 38 | -------------------------------------------------------------------------------- /error.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 39 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | 48 | -------------------------------------------------------------------------------- /components/NxDropdown.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | -------------------------------------------------------------------------------- /iam/authz/permissions.ts: -------------------------------------------------------------------------------- 1 | /* Permissions should always return true or false. Any error that occurs, should print to the console and return false */ 2 | /* They are designed to be independent checks. However because they may do very similar things, they can become expensive*/ 3 | 4 | import { User } from "~~/iam/misc/types"; 5 | 6 | /** 7 | * @desc Checks if a user has admin authorization 8 | * @param user User object 9 | */ 10 | export function canAccessAdmin(user: User): boolean{ 11 | if (user.role === 'SUPER_ADMIN' && user.email_verified) 12 | return true 13 | 14 | return false 15 | } 16 | 17 | /** 18 | * @desc Checks if a user has a permission 19 | * @param user User object 20 | * @param permission A permission 21 | */ 22 | export function hasPermission(user: User, permission: string): boolean { 23 | // Check if user has the permission 24 | const permissions = { 25 | 'can-access-admin': canAccessAdmin(user), 26 | } 27 | 28 | // If permission does not exist, return false 29 | if (permission in permissions === false) { 30 | console.log(`No such permission as "${permission}"`) 31 | return false 32 | } 33 | else { 34 | // @ts-ignore 35 | return permissions[permission] 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-iam", 3 | "version": "0.1.0", 4 | "description": "Nuxt authentication and authorization framework providing identity and access management.", 5 | "keywords": [ 6 | "nuxt auth", 7 | "auth", 8 | "authentication", 9 | "nuxt", 10 | "nuxt 3", 11 | "authorization", 12 | "nuxt auth 3" 13 | ], 14 | "author": "Jeremy Mwangelwa", 15 | "license": "MIT", 16 | "private": false, 17 | "scripts": { 18 | "build": "nuxt build", 19 | "dev": "nuxt dev", 20 | "generate": "nuxt generate", 21 | "preview": "nuxt preview", 22 | "postinstall": "nuxt prepare" 23 | }, 24 | "devDependencies": { 25 | "nuxt": "3.3.2" 26 | }, 27 | "dependencies": { 28 | "@pinia/nuxt": "^0.4.7", 29 | "@prisma/client": "^4.12.0", 30 | "@sendgrid/mail": "^7.7.0", 31 | "@types/jsonwebtoken": "^8.5.9", 32 | "@types/nodemailer": "^6.4.7", 33 | "@types/uuid": "^9.0.0", 34 | "argon2": "^0.30.2", 35 | "dayjs": "^1.11.7", 36 | "generate-password": "^1.7.0", 37 | "google-auth-library": "^8.7.0", 38 | "jsonwebtoken": "^9.0.0", 39 | "nodemailer": "^6.8.0", 40 | "nuxt-vue3-google-signin": "^0.0.8", 41 | "pinia": "^2.0.33", 42 | "prisma": "^4.8.1", 43 | "save-dev": "^0.0.1-security", 44 | "uuid": "^9.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /iam/mvc/doodads/controller.ts: -------------------------------------------------------------------------------- 1 | /* Example doodads controller 2 | * Routes all doodad requests 3 | */ 4 | 5 | // Definitely much cleaner and neater 6 | 7 | import { index, create, show, update, destroy } from "./model"; 8 | import { createRouter, defineEventHandler, useBase } from 'h3'; 9 | 10 | const router = createRouter(); 11 | 12 | // Routes /api/iam/doodads 13 | 14 | // Get all doodads 15 | router.get('/', defineEventHandler(async (event) => { 16 | return await index(event) 17 | })); 18 | 19 | // Create a doodad 20 | router.post('/', defineEventHandler(async (event) => { 21 | return await create(event) 22 | })); 23 | 24 | // Get a single doodad 25 | router.get('/:id', defineEventHandler(async (event) => { 26 | return await show(event) 27 | })); 28 | 29 | // Edit a doodad 30 | router.put('/:id', defineEventHandler(async (event) => { 31 | return await update(event) 32 | })); 33 | 34 | // Delete a doodad 35 | router.delete('/:id', defineEventHandler(async (event) => { 36 | return await destroy(event) 37 | })); 38 | 39 | // Example complex route 40 | router.get('/:id/abc/:author-id', defineEventHandler((event) => { 41 | const headers = getHeaders(event); 42 | return { 43 | params: event.context.params, 44 | headers: headers, 45 | } 46 | })); 47 | 48 | export default useBase('/api/iam/doodads', router.handler); 49 | -------------------------------------------------------------------------------- /iam/middleware/index.ts: -------------------------------------------------------------------------------- 1 | /** Global middleware for Mulozi 2 | */ 3 | import { H3Event, H3Error } from "h3"; 4 | 5 | /** 6 | * @desc Checks header for client-platform value ('app', 'browser', or 'browser-dev') 7 | * @param event H3 Event passed from api 8 | * @info Allows us to create a more secure backend for frontend strategy 9 | * @returns {} Object mentioning success or failure of refreshing user's tokens 10 | */ 11 | export function getClientPlatform(event: H3Event): H3Error | string { 12 | const clientPlatforms = ["app", "browser", "browser-dev"]; 13 | let clientPlatform = event.node.req.headers["client-platform"] as string; 14 | 15 | // Check if 'client-platform' header is present 16 | if (!clientPlatform) { 17 | console.log( 18 | "Missing required header 'client-platform'. 'client-platform' upgraded to 'browser'" 19 | ); 20 | 21 | // Add client-platform to request headers 22 | event.node.req.headers["client-platform"] = "browser"; 23 | clientPlatform = "browser"; 24 | } 25 | 26 | // Check if 'client-platform' header is either 'app' or 'browser' 27 | if (!clientPlatforms.includes(clientPlatform)) 28 | return createError({ 29 | statusCode: 400, 30 | statusMessage: 31 | "Required header 'client-platform' must be 'app', 'browser', or 'browser-dev' only", 32 | }); 33 | 34 | return clientPlatform; 35 | } 36 | -------------------------------------------------------------------------------- /stores/useIamProfileStore.ts: -------------------------------------------------------------------------------- 1 | // Pinia store to store user profile 2 | //! Do not store sensitive data in here. 3 | //! Avoid storing permissions. Rather, always check permissions from backend. 4 | 5 | import { User, NxFormInput } from "~~/iam/misc/types"; 6 | import { defineStore } from "pinia"; 7 | 8 | export const useIamProfileStore = defineStore("iamProfile", () => { 9 | const myProfile = ref(null); 10 | const isLoggedIn = ref(false); 11 | const updateCount = ref(0); 12 | 13 | // Returns the profile 14 | const getProfile = computed(() => myProfile.value); 15 | 16 | /** 17 | * @desc Sets profile 18 | * @param profile 19 | */ 20 | function setProfile(profile: User) { 21 | if (profile) myProfile.value = profile; 22 | } 23 | 24 | /** 25 | * @desc Sets whether user is logged in 26 | */ 27 | function setIsLoggedIn(value: boolean) { 28 | isLoggedIn.value = value; 29 | } 30 | 31 | /** 32 | * @desc Clears profile 33 | */ 34 | function clearProfile() { 35 | myProfile.value = null; 36 | } 37 | 38 | /** 39 | * @desc Increases updateCount whenever an update is made 40 | */ 41 | function setUpdateCount() { 42 | updateCount.value++; 43 | } 44 | 45 | return { 46 | setProfile, 47 | getProfile, 48 | setIsLoggedIn, 49 | isLoggedIn, 50 | clearProfile, 51 | setUpdateCount, 52 | updateCount, 53 | }; 54 | }); 55 | -------------------------------------------------------------------------------- /pages/iam/dashboard/admin.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /components/NxAvatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | 37 | -------------------------------------------------------------------------------- /server/middleware/iam/authentication/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if request url is a url that requires authentication 3 | * If user is authenticated, adds user to context 4 | */ 5 | 6 | import { isAuthenticated } from "~~/iam/mvc/authn/queries"; 7 | import { getAuthenticatedRoutes} from "~~/iam/misc/utils/logins" 8 | import { getUserFromAccessToken } from "~~/iam/authz/helpers"; 9 | 10 | import { H3Error } from "h3"; 11 | 12 | const forbiddenError = createError({ 13 | statusCode: 403, 14 | statusMessage: "Forbidden", 15 | }); 16 | 17 | export default defineEventHandler(async (event) => { 18 | 19 | // Get all routes that need a user to be authenticated 20 | const authRoutes = getAuthenticatedRoutes() 21 | 22 | // Check if request url is among authenticated routes 23 | if (event.node.req.url) 24 | for (let i = 0; i < authRoutes.length; i++) { 25 | if (event.node.req.url.includes(authRoutes[i])) { 26 | // Check if user is authenticated 27 | const authenticated = await isAuthenticated(event); 28 | 29 | if (authenticated instanceof H3Error) 30 | throw forbiddenError 31 | 32 | if (authenticated === false) 33 | throw forbiddenError 34 | 35 | // If user is authenticated, add user to context 36 | const userOrNull = await getUserFromAccessToken(event); 37 | 38 | if (userOrNull === null) { 39 | console.log('Missing access token after authentication. This should not happen.') 40 | throw createError({ 41 | statusCode: 401, 42 | statusMessage: "Unauthorized. Missing access token." 43 | }); 44 | } 45 | 46 | // Add user to context 47 | event.context.user = userOrNull 48 | break 49 | } 50 | } 51 | 52 | 53 | }) 54 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | css: [ 4 | // CSS file in the project 5 | "@/assets/iam/resources/css/style.css", 6 | ], 7 | 8 | runtimeConfig: { 9 | // IAM token secrets. Please rotate every 2 - 4 weeks 10 | iamAccessTokenSecret: process.env.IAM_ACCESS_TOKEN_SECRET, 11 | iamRefreshTokenSecret: process.env.IAM_REFRESH_TOKEN_SECRET, 12 | iamResetTokenSecret: process.env.IAM_RESET_TOKEN_SECRET, 13 | iamVerifyTokenSecret: process.env.IAM_VERIFY_TOKEN_SECRET, 14 | 15 | // Public Url 16 | iamPublicUrl: process.env.IAM_PUBLIC_URL, 17 | 18 | // IAM Emailer 19 | iamEmailer: process.env.IAM_EMAILER, 20 | 21 | // nodemailer-service 22 | iamNodemailerService: process.env.IAM_NODEMAILER_SERVICE, 23 | iamNodemailerServiceSender: process.env.IAM_NODEMAILER_SERVICE_SENDER, 24 | iamNodemailerServicePassword: process.env.IAM_NODEMAILER_SERVICE_PASSWORD, 25 | 26 | // nodemailer-smtp 27 | iamNodemailerSmtpHost: process.env.IAM_NODEMAILER_SMTP_HOST, 28 | iamNodemailerSmtpPort: process.env.IAM_NODEMAILER_SMTP_PORT, 29 | iamNodemailerSmtpSender: process.env.IAM_NODEMAILER_SMTP_SENDER, 30 | iamNodemailerSmtpPassword: process.env.IAM_NODEMAILER_SMTP_PASSWORD, 31 | 32 | // IAM SendGrid 33 | iamSendGridApiKey: process.env.IAM_SENDGRID_API_KEY, 34 | iamSendgridSender: process.env.IAM_SENDGRID_SENDER, 35 | 36 | // GOOGLE CLIENT ID 37 | iamGoogleClientId: process.env.IAM_GOOGLE_CLIENT_ID, 38 | 39 | // Do not put secret information here 40 | public: { 41 | iamVerifyRegistrations: process.env.IAM_VERIFY_REGISTRATIONS, 42 | iamAllowGoogleAuth: process.env.IAM_ALLOW_GOOGLE_AUTH, 43 | }, 44 | }, 45 | 46 | // Modules 47 | modules: [ 48 | "nuxt-vue3-google-signin", 49 | "@pinia/nuxt", 50 | ], 51 | 52 | // Google sign in 53 | googleSignIn: { 54 | clientId: process.env.IAM_GOOGLE_CLIENT_ID, 55 | }, 56 | 57 | typescript: { 58 | shim: false, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /pages/iam/docs/getting-started.vue: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /components/NxModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | -------------------------------------------------------------------------------- /iam/mvc/refresh-tokens/controller.ts: -------------------------------------------------------------------------------- 1 | /* Users controller 2 | * Routes all user requests 3 | */ 4 | 5 | import { index, destroy, destroyAll } from "./model"; 6 | import { createRouter, defineEventHandler, useBase, H3Error } from 'h3'; 7 | import { canAccessAdmin } from "~~/iam/authz/permissions"; 8 | import { validateCsrfToken } from "~~/iam/misc/utils/tokens"; 9 | 10 | // User not found error 11 | const userNotFoundError = createError({ 12 | statusCode: 401, 13 | statusMessage: "Unauthorized. User not found.", 14 | }); 15 | 16 | // Forbidden error 17 | const forbiddenError = createError({ 18 | statusCode: 403, 19 | statusMessage: "Forbidden", 20 | }); 21 | 22 | // Missing csrf token error 23 | const csrfTokenError = createError({ 24 | statusCode: 403, 25 | statusMessage: "Missing or invalid csrf token", 26 | }); 27 | 28 | const router = createRouter(); 29 | 30 | // Get all refresh tokens 31 | router.get('/', defineEventHandler(async (event) => { 32 | // TODO: Change this to one permission like isAdminAuthorized 33 | if (!event.context.user) throw userNotFoundError 34 | if (!canAccessAdmin(event.context.user)) throw forbiddenError 35 | return await index(event) 36 | })); 37 | 38 | // Delete a refresh token 39 | router.delete('/:id', defineEventHandler(async (event) => { 40 | // Check if csrf token is valid 41 | const tokenOrError = await validateCsrfToken(event); 42 | if (tokenOrError instanceof H3Error) throw csrfTokenError; 43 | 44 | // Get user from context 45 | if (!event.context.user) throw userNotFoundError 46 | if (!canAccessAdmin(event.context.user)) throw forbiddenError 47 | 48 | return await destroy(event) 49 | })); 50 | 51 | // Delete all refresh tokens 52 | router.delete('/', defineEventHandler(async (event) => { 53 | // Check if csrf token is valid 54 | const tokenOrError = await validateCsrfToken(event); 55 | if (tokenOrError instanceof H3Error) throw csrfTokenError; 56 | 57 | // Get user from context 58 | if (!event.context.user) throw userNotFoundError 59 | if (!canAccessAdmin(event.context.user)) throw forbiddenError 60 | 61 | return await destroyAll(event) 62 | })); 63 | 64 | export default useBase('/api/iam/refresh-tokens', router.handler); 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 95 | 96 | 116 | -------------------------------------------------------------------------------- /pages/iam/docs/index.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /iam/mvc/doodads/model.ts: -------------------------------------------------------------------------------- 1 | /* Example doodads model 2 | * Data manipulation of all doodad requests 3 | */ 4 | 5 | import { H3Event } from "h3"; 6 | import { JSONResponse } from "~~/iam/misc/types"; 7 | 8 | 9 | /** 10 | * @desc Shows all doodads 11 | * @param event H3 Event passed from api 12 | * @returns {Promise} Returns doodads or error 13 | */ 14 | export async function index(event: H3Event): Promise { 15 | const response = {} as JSONResponse; 16 | 17 | const info = "get all doodads" 18 | response.status = "success"; 19 | response.data = { 20 | info, 21 | }; 22 | 23 | return response; 24 | } 25 | 26 | /** 27 | * @desc Creates a new doodad in database 28 | * @param event H3 Event passed from api 29 | * @returns {Promise} 30 | */ 31 | export async function create(event: H3Event): Promise { 32 | const response = {} as JSONResponse; 33 | 34 | const info = "create a doodad" 35 | response.status = "success"; 36 | response.data = { 37 | info 38 | }; 39 | 40 | return response; 41 | } 42 | 43 | /** 44 | * @desc Show a particular doodad 45 | * @param event H3 Event passed from api 46 | * @returns {Promise} doodad object or error 47 | */ 48 | export async function show(event: H3Event): Promise { 49 | const response = {} as JSONResponse; 50 | 51 | const info = "show a doodad" 52 | response.status = "success"; 53 | response.data = { 54 | info 55 | }; 56 | 57 | return response; 58 | } 59 | 60 | /** 61 | * @desc Update particular doodad 62 | * @param event H3 Event passed from api 63 | * @returns {Promise} Object mentioning success or failure of editing doodad or error 64 | */ 65 | export async function update(event: H3Event): Promise { 66 | const response = {} as JSONResponse; 67 | 68 | const info = "update a doodad" 69 | response.status = "success"; 70 | response.data = { 71 | info 72 | }; 73 | 74 | return response; 75 | } 76 | 77 | /** 78 | * @desc Delete a particular doodad 79 | * @param event H3 Event passed from api 80 | * @returns {Promise} Object mentioning success or failure of deleting doodad or error 81 | */ 82 | export async function destroy(event: H3Event): Promise { 83 | const response = {} as JSONResponse; 84 | 85 | const info = "delete a doodad" 86 | response.status = "success"; 87 | response.data = { 88 | info 89 | }; 90 | 91 | return response; 92 | } 93 | -------------------------------------------------------------------------------- /components/iamReset.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | # PRISMA DATABASE 8 | # DATABASE_URL="mysql://root:@localhost:3306/nuxtauth" 9 | DATABASE_URL="mysql://dbuser:dbpassword@dbserver:dbport/dbname" 10 | 11 | # NUXT IAM DOCUMENTATION 12 | # For full documentations, go to [Nuxt IAM documentation](https://nuxt-iam.vercel.app/iam/) 13 | 14 | # NUXT IAM TOKEN SECRETS (Please change them every 2 - 4 weeks) 15 | # Can create in node 'crypto.randomBytes(64).toString('hex')' 16 | IAM_ACCESS_TOKEN_SECRET="fa85424538b2878a7785a703d168fc58550c8ef3a02c23b8aee5f8adf98159b218296926d37164db8ed48d28a73c01387cf4fd0032e7e76858e71a09b2b82c88" 17 | IAM_REFRESH_TOKEN_SECRET="c36f673adcbfe27859867697d6c98430c3757d3eafb7bca3fe90fe349baa9d88ec10932aba5da22f37264c2c2bd31404e5a22822be6f054ec9d40a56b28b97e1" 18 | IAM_RESET_TOKEN_SECRET="a67102c7d684ad370409855fe3e7a65f9f9ccffeb82b0d53fe328c66a1405b03737da79a4dd42266a5a83ea9826421b9b031703337be5fd1e1eac0feb1ae2166" 19 | IAM_VERIFY_TOKEN_SECRET="823459ed2ed1d80df1aedd3a5c03f1ee6132c20a076270d63d89e06c6b0f4fb7299991610f17dde4930b54432a6cf7b8d13b6da08b9f89bb8b30bb2c46c37f9e" 20 | 21 | # NUXT IAM 22 | # If using a browser like your Nuxt app, use 'browser' for production 23 | # If using a browser like your Nuxt app, use 'browser-dev' for development 24 | # If you're not using a browser, then use 'app' 25 | IAM_CLIENT_PLATFORM="browser" 26 | 27 | IAM_PUBLIC_URL="http://localhost:3000" 28 | 29 | # NUXT IAM RESET EMAIL 30 | # nodemailer-service, nodemailer-smtp, sendgrid 31 | IAM_EMAILER="nodemailer-smtp" 32 | 33 | # nodemailer-service 34 | IAM_NODEMAILER_SERVICE="hotmail" 35 | IAM_NODEMAILER_SERVICE_SENDER="myusername@outlook.com" 36 | IAM_NODEMAILER_SERVICE_PASSWORD="myExcellentPassword767*" 37 | 38 | # nodemailer-smtp 39 | IAM_NODEMAILER_SMTP_HOST="mysmtp.host" 40 | IAM_NODEMAILER_SMTP_PORT="465" 41 | IAM_NODEMAILER_SMTP_SENDER="myname@mydomain.com" 42 | IAM_NODEMAILER_SMTP_PASSWORD="myAmazingPassword753$" 43 | 44 | # NUXT IAM VERIFY REGISTRATIONS 45 | IAM_VERIFY_REGISTRATIONS="false" 46 | 47 | # SENDGRID API KEY 48 | IAM_SENDGRID_API_KEY="12345678901234567890" 49 | IAM_SENDGRID_SENDER="myname@mysendgridaccount.com" 50 | 51 | # IAM GOOGLE CLIENT ID 52 | IAM_ALLOW_GOOGLE_AUTH="false" 53 | IAM_GOOGLE_CLIENT_ID="123...com" -------------------------------------------------------------------------------- /iam/mvc/refresh-tokens/model.ts: -------------------------------------------------------------------------------- 1 | import { H3Event, H3Error } from "h3"; 2 | import { 3 | getAllRefreshTokens, 4 | destroyRefreshToken, 5 | destroyRefreshTokens, 6 | } from "./queries"; 7 | import { JSONResponse, RefreshTokens } from "~~/iam/misc/types"; 8 | 9 | /** 10 | * @desc Gets all refresh tokens 11 | * @param event H3 Event passed from api 12 | * @returns {Promise} Returns success or failure 13 | */ 14 | export async function index(event: H3Event): Promise { 15 | const response = {} as JSONResponse; 16 | const errorOrTokens = await getAllRefreshTokens(event); 17 | 18 | // If error, return error 19 | if (errorOrTokens instanceof H3Error) { 20 | response.status = "fail"; 21 | response.error = errorOrTokens; 22 | return response; 23 | } 24 | 25 | // Otherwise, return tokens 26 | const tokens = errorOrTokens as RefreshTokens; 27 | response.status = "success"; 28 | response.data = tokens; 29 | 30 | return response; 31 | } 32 | 33 | /** 34 | * @desc Delete a particular refresh token 35 | * @param event H3 Event passed from api 36 | * @returns {Promise} Returns success of failure 37 | */ 38 | export async function destroy(event: H3Event): Promise { 39 | const response = {} as JSONResponse; 40 | const errorOrBoolean = await destroyRefreshToken(event); 41 | 42 | // If error, return error 43 | if (errorOrBoolean instanceof H3Error) { 44 | response.status = "fail"; 45 | response.error = errorOrBoolean; 46 | return response; 47 | } 48 | 49 | // If false is returned, which shouldn't happen 50 | if (errorOrBoolean === false) { 51 | response.status = "fail"; 52 | return response; 53 | } 54 | // Otherwise token successfully deleted 55 | else { 56 | response.status = "success"; 57 | return response; 58 | } 59 | } 60 | 61 | /** 62 | * @desc Delete allrefresh token 63 | * @param event H3 Event passed from api 64 | * @returns {Promise} Returns success of failure 65 | */ 66 | export async function destroyAll(event: H3Event): Promise { 67 | const response = {} as JSONResponse; 68 | const errorOrBoolean = await destroyRefreshTokens(event); 69 | 70 | // If error, return error 71 | if (errorOrBoolean instanceof H3Error) { 72 | response.status = "fail"; 73 | response.error = errorOrBoolean; 74 | return response; 75 | } 76 | 77 | // If false is returned, which shouldn't happen 78 | if (errorOrBoolean === false) { 79 | response.status = "fail"; 80 | return response; 81 | } 82 | // Otherwise token successfully deleted 83 | else { 84 | response.status = "success"; 85 | return response; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-iam - Nuxt authentication framework 2 | 3 | ## Download from **[Github](https://github.com/jeremycoder/nuxt-iam)** NOT npm. 4 | 5 | ## **!! NOTE: Code is NO LONGER actively maintained. Use at own risk!!**. 6 | 7 | Nuxt IAM, which stands for Nuxt Identity and Access Management, is an authentication and authorization framework for Nuxt that allows you to secure your app with industry best practices. Nuxt IAM, adds authentication and authorization logic to your Nuxt app. 8 | 9 | See a fully functional [example app](https://nuxtiam.com/). 10 | 11 | https://user-images.githubusercontent.com/7818102/224216154-9b8672e0-f195-4d41-aa15-3b268d65b214.mp4 12 | 13 | Nuxt IAM is a Nuxt app that contains the following authentication and authorization features: 14 | 15 | - ✔️ user registration with email and password 16 | - ✔️ user login with email and password 17 | - ✔️ user login/registration with Google 18 | - ✔️ user password reset 19 | - ✔️ user dashboard 20 | - ✔️ user password change 21 | - ✔️ user profile/account delete 22 | - ✔️ admin user management 23 | - ✔️ admin token management 24 | 25 | It is a full featured Nuxt 3 app. 26 | 27 | For full documentations, go to [Nuxt IAM documentation](https://nuxt-iam.vercel.app/iam/) 28 | 29 | ## How it Works 30 | 31 | Simply clone the [Github repo](https://github.com/jeremycoder/nuxt-iam), fork it, or download it. 32 | 33 | ## Getting Started 34 | 35 | Nuxt IAM is a Nuxt application and comes ready to run. All you need to add is a database. 36 | 37 | 1. Please install [Node](https://nodejs.org) if you don't already have it. The recommended Node version is **16.16 or greater** 38 | 2. Please install [Yarn package manager](https://yarnpkg.com/). (You can also use npm if you like, but we prefer Yarn) 39 | 3. Clone, fork, or download the repo from `https://github.com/jeremycoder/nuxt-iam`, and navigate to the root directory. 40 | 4. Copy the `.env.example` file and create a `.env` file 41 | 5. Run `yarn` or `yarn install`. 42 | 6. Add your database information to your `.env` file. Nuxt IAM curently supports MySQL, but can be modified to support other databases. See [Prisma](https://www.prisma.io/docs/reference/database-reference/connection-urls) for more information. 43 | 7. Connect your app to your database by running `npx prisma migrate dev`. Name your migration `initial_migration` or something similar 44 | 8. Run `yarn dev`, and you're good to go! 45 | 46 | More [configuration](https://nuxt-iam.vercel.app/iam/docs/configuration) is required if you need to send emails and use Google authentication. 47 | 48 | Learn more about how Nuxt IAM works by looking at the [concepts](https://nuxt-iam.vercel.app/iam/docs/concepts). 49 | 50 | Check out the sample app here: https://nuxt-iam.vercel.app/iam/ 51 | 52 | For documentation: https://nuxt-iam.vercel.app/iam/docs 53 | -------------------------------------------------------------------------------- /iam/misc/types.d.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: number; 3 | first_name: string; 4 | last_name: string; 5 | uuid: string; 6 | email: string; 7 | password: string; 8 | permissions: string | null; 9 | avatar?: string | null; 10 | role: "SUPER_ADMIN" | "ADMIN" | "GENERAL"; 11 | csrf_token?: string; 12 | current_password?: string; 13 | new_password?: string; 14 | email_verified: boolean; 15 | is_active: boolean; 16 | last_login: Date | null; 17 | created_at: Date; 18 | }; 19 | 20 | export type ProviderUser = { 21 | id: number; 22 | provider: "GOOGLE"; 23 | provider_user_id: string; 24 | user_id: number; 25 | }; 26 | 27 | export type JSONResponse = { 28 | status: "success" | "fail"; 29 | data?: any; 30 | error?: any; 31 | }; 32 | 33 | export type TokensSession = { 34 | accessToken: string; 35 | refreshToken: string; 36 | sid?: string; 37 | }; 38 | 39 | export type ClientPlatforms = "app" | "browser" | "browser-dev"; 40 | 41 | export type EmailOptions = { 42 | to: string; 43 | from: string; 44 | subject: string; 45 | text?: string; 46 | html?: string; 47 | }; 48 | 49 | export type UserEditable = { 50 | first_name?: string; 51 | last_name?: string; 52 | role?: string; 53 | csrf_token?: string; 54 | is_active?: boolean; 55 | permissions?: string; 56 | }; 57 | 58 | export type NewUser = { 59 | first_name: string; 60 | last_name: string; 61 | email: string; 62 | password: string; 63 | csrf_token?: string; 64 | }; 65 | 66 | export type RefreshToken = { 67 | id: number; 68 | token_id: string; 69 | user_id: number; 70 | is_active: boolean; 71 | date_created: DateTime; 72 | }; 73 | 74 | export type RefreshTokens = Array; 75 | 76 | export type Session = { 77 | id: number; 78 | user_id: number; 79 | sid: string; 80 | start_time: DateTime; 81 | end_time?: DateTime; 82 | access_token: string; 83 | csrf_token: string; 84 | is_active: boolean; 85 | ip_address: string; 86 | }; 87 | 88 | export enum Roles { 89 | "SUPER_ADMIN", 90 | "ADMIN", 91 | "GENERAL", 92 | } 93 | 94 | export type NxFormInput = { 95 | label?: string; 96 | id: string; 97 | type?: 98 | | "input:text" 99 | | "input:password" 100 | | "input:email" 101 | | "input:number" 102 | | "textarea" 103 | | "select"; 104 | options?: Array; 105 | disabled?: boolean; 106 | show?: boolean; 107 | value?: string; 108 | }; 109 | 110 | export type NxLink = { 111 | name: string; 112 | link?: string; 113 | disabled?: boolean; 114 | show?: boolean; 115 | hasBorder?: boolean; 116 | showChildren?: boolean; 117 | children?: Links; 118 | bold?: boolean; 119 | group?: string; 120 | }; 121 | 122 | export type NxLinks = Array; 123 | -------------------------------------------------------------------------------- /iam/authz/helpers.ts: -------------------------------------------------------------------------------- 1 | // Helper functions for authorization 2 | 3 | import { H3Event, H3Error } from "h3"; 4 | import { verifyAccessToken } from "../misc/utils/tokens"; 5 | import { getUserByUuid } from "../misc/utils/users"; 6 | import { JwtPayload } from "jsonwebtoken"; 7 | import { User } from "~~/iam/misc/types"; 8 | 9 | /** 10 | * @desc Determines if user can read their own user record 11 | * @param userUuid User uuid 12 | * @param routeUuid User uuid from the route 13 | */ 14 | export function isOwner(userUuid: string, routeUuid: string): boolean { 15 | if (userUuid !== routeUuid) { 16 | console.log("Authorization failed. User is not owner of record."); 17 | return false; 18 | } 19 | 20 | return true; 21 | } 22 | 23 | /** 24 | * @desc Gets user from access token 25 | * @param event Event from api 26 | */ 27 | 28 | export async function getUserFromAccessToken( 29 | event: H3Event 30 | ): Promise { 31 | let accessToken = null; 32 | let tokenPayload = null; 33 | 34 | console.log('Attempt to get user from access token') 35 | 36 | // Client platform if not using Nuxt front end 37 | let clientPlatform = event.node.req.headers["client-platform"] as string; 38 | 39 | // If no client platform, upgrade to browser 40 | if (!clientPlatform) clientPlatform = "browser" 41 | 42 | // If client platform is app, get access token from headers 43 | if (clientPlatform === "app") 44 | accessToken = event.node.req.headers["iam-access-token"] as string; 45 | // Otherwise, get it from cookies 46 | else if (["browser", "browser-dev"].includes(clientPlatform)) { 47 | accessToken = getCookie(event, "iam-access-token") as string; 48 | } 49 | // If that fails, value is invalid 50 | else { 51 | console.log("Invalid client platform: ", clientPlatform); 52 | return null; 53 | } 54 | 55 | // If no token, display error and return false 56 | if (!accessToken) { 57 | console.log("No access token provided. Cannot get user from access token"); 58 | return null; 59 | } 60 | 61 | // Verify access token 62 | const accessTokenArr = accessToken.split(" "); 63 | const errorOrToken = verifyAccessToken(accessTokenArr[1]); 64 | 65 | // If error, print to console and return false 66 | if (errorOrToken instanceof H3Error) { 67 | console.log(errorOrToken); 68 | console.log("Error verifying access token"); 69 | return null; 70 | } 71 | 72 | // Otherwise, get token payload 73 | tokenPayload = errorOrToken as JwtPayload; 74 | 75 | // Get user by uuid 76 | const userOrNull = await getUserByUuid(tokenPayload.uuid); 77 | 78 | // If no user, show error, return false 79 | if (userOrNull === null) { 80 | console.log("Failed to get user to check for isSuperAdmin"); 81 | return null; 82 | } 83 | 84 | // Otherwise get and return 85 | const user = userOrNull as User; 86 | return user; 87 | } -------------------------------------------------------------------------------- /iam/mvc/authn/controller.ts: -------------------------------------------------------------------------------- 1 | /* Authentication controller 2 | * Routes all authentication requests 3 | */ 4 | import { createRouter, defineEventHandler } from "h3"; 5 | import { 6 | register, 7 | login, 8 | loginWithGoogle, 9 | profile, 10 | update, 11 | refresh, 12 | logout, 13 | isauthenticated, 14 | destroy, 15 | reset, 16 | verifyReset, 17 | verifyEmail, 18 | verifyEmailToken, 19 | } from "./model"; 20 | 21 | const router = createRouter(); 22 | 23 | // Get user profile 24 | router.get( 25 | "/profile", 26 | defineEventHandler(async (event) => { 27 | return await profile(event); 28 | }) 29 | ); 30 | 31 | // Check if user is authenticated 32 | router.get( 33 | "/isauthenticated", 34 | defineEventHandler(async (event) => { 35 | return await isauthenticated(event); 36 | }) 37 | ); 38 | 39 | // Get user profile 40 | router.post( 41 | "/register", 42 | defineEventHandler(async (event) => { 43 | return await register(event); 44 | }) 45 | ); 46 | 47 | // Log user in 48 | router.post( 49 | "/login", 50 | defineEventHandler(async (event) => { 51 | return await login(event); 52 | }) 53 | ); 54 | 55 | // Login with Google 56 | router.post( 57 | "/login-google", 58 | defineEventHandler(async (event) => { 59 | return await loginWithGoogle(event); 60 | }) 61 | ); 62 | 63 | // Refresh JSON web tokens 64 | router.post( 65 | "/refresh", 66 | defineEventHandler(async (event) => { 67 | return await refresh(event); 68 | }) 69 | ); 70 | 71 | // Verify password reset token 72 | router.post( 73 | "/reset", 74 | defineEventHandler(async (event) => { 75 | return await reset(event); 76 | }) 77 | ); 78 | 79 | // Receive token from user's email link and verify it 80 | router.post( 81 | "/verifyreset", 82 | defineEventHandler(async (event) => { 83 | return await verifyReset(event); 84 | }) 85 | ); 86 | 87 | // Send email to verify user email 88 | router.post( 89 | "/verifyemail", 90 | defineEventHandler(async (event) => { 91 | return await verifyEmail(event); 92 | }) 93 | ); 94 | 95 | // Verify token sent from user's email verification link 96 | router.post( 97 | "/verifyemailtoken", 98 | defineEventHandler(async (event) => { 99 | return await verifyEmailToken(event); 100 | }) 101 | ); 102 | 103 | // logout 104 | router.post( 105 | "/logout", 106 | defineEventHandler(async (event) => { 107 | return await logout(event); 108 | }) 109 | ); 110 | 111 | // Update user profile 112 | router.put( 113 | "/update", 114 | defineEventHandler(async (event) => { 115 | return await update(event); 116 | }) 117 | ); 118 | 119 | // Delete user profile 120 | router.delete( 121 | "/delete", 122 | defineEventHandler(async (event) => { 123 | return await destroy(event); 124 | }) 125 | ); 126 | 127 | export default useBase("/api/iam/authn", router.handler); 128 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model users { 11 | id Int @id @default(autoincrement()) 12 | uuid String @unique(map: "uuid") @db.VarChar(60) 13 | email String @unique(map: "email") @db.VarChar(255) 14 | password String @db.VarChar(255) 15 | avatar String? @db.VarChar(1000) 16 | permissions String? @db.VarChar(4000) 17 | first_name String @db.VarChar(255) 18 | last_name String @db.VarChar(255) 19 | role Role @default(GENERAL) 20 | email_verified Boolean @default(false) 21 | is_active Boolean @default(true) 22 | last_login DateTime? @db.DateTime(0) 23 | created_at DateTime @default(now()) @db.DateTime(0) 24 | deleted_at DateTime? @db.DateTime(0) 25 | updated_at DateTime? @updatedAt 26 | refresh_tokens refresh_tokens[] 27 | sessions sessions[] 28 | provider_users provider_users[] 29 | } 30 | 31 | model provider_users { 32 | id Int @id @default(autoincrement()) 33 | provider Provider 34 | provider_user_id String @unique(map: "provider_user_id") 35 | user users? @relation(fields: [user_id], references: [id], onDelete: Cascade) 36 | user_id Int 37 | updated_at DateTime? @updatedAt 38 | } 39 | 40 | model sessions { 41 | id Int @id @default(autoincrement()) 42 | user users? @relation(fields: [user_id], references: [id], onDelete: Cascade) 43 | user_id Int 44 | sid String @unique(map: "sid") 45 | start_time DateTime @default(now()) 46 | end_time DateTime? 47 | access_token String @db.VarChar(4000) 48 | csrf_token String @db.VarChar(255) 49 | is_active Boolean 50 | ip_address String 51 | updated_at DateTime? @updatedAt 52 | } 53 | 54 | enum Role { 55 | SUPER_ADMIN 56 | ADMIN 57 | GENERAL 58 | } 59 | 60 | enum Provider { 61 | GOOGLE 62 | } 63 | 64 | model refresh_tokens { 65 | id Int @id @default(autoincrement()) 66 | token_id String @unique(map: "token_id") @db.VarChar(60) 67 | user users? @relation(fields: [user_id], references: [id], onDelete: Cascade) 68 | user_id Int 69 | is_active Boolean 70 | date_created DateTime @default(now()) @db.DateTime(0) 71 | updated_at DateTime? @updatedAt 72 | } 73 | 74 | model one_time_tokens { 75 | id Int @id @default(autoincrement()) 76 | token_id String @unique(map: "token_id") @db.VarChar(60) 77 | token_type tokenType? 78 | expires_at DateTime @db.DateTime(0) 79 | date_created DateTime @default(now()) @db.DateTime(0) 80 | updated_at DateTime? @updatedAt 81 | } 82 | 83 | enum tokenType { 84 | RESET 85 | } 86 | -------------------------------------------------------------------------------- /components/NxCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 67 | 68 | -------------------------------------------------------------------------------- /components/NxObjectAsTable.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 97 | 98 | -------------------------------------------------------------------------------- /components/NxMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 80 | 81 | -------------------------------------------------------------------------------- /iam/misc/utils/passwords.ts: -------------------------------------------------------------------------------- 1 | import argon2 from "argon2"; 2 | import { PrismaClient } from "@prisma/client"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | import { H3Error } from "h3"; 5 | import crypto from "crypto"; 6 | import { validatePassword } from "./../utils/validators"; 7 | import passwordGenerator from "generate-password"; 8 | 9 | const prisma = new PrismaClient(); 10 | 11 | /** 12 | * @desc Returns a random string of 32 characters in hexadecimal 13 | * @info Can be used to create a secret 14 | */ 15 | export function makeRandomString32(): string { 16 | return crypto.randomBytes(32).toString("hex"); 17 | } 18 | 19 | /** 20 | * @desc Hashes a password or any string using Argon 2 21 | * @param password Unhashed password 22 | */ 23 | export async function hashPassword( 24 | password: string 25 | ): Promise { 26 | try { 27 | return await argon2.hash(password); 28 | } catch (err) { 29 | return createError({ statusCode: 500, statusMessage: "Password error" }); 30 | } 31 | } 32 | 33 | /** 34 | * @desc Makes a uuid 35 | */ 36 | export function makeUuid(): string { 37 | return uuidv4(); 38 | } 39 | 40 | /** 41 | * @Desc Generates a new password for user given user's uuid 42 | * @param uuid User's uuid 43 | * @returns {Promise} Returns generated password or error 44 | */ 45 | export async function generateNewPassword( 46 | uuid: string 47 | ): Promise { 48 | let error = null; 49 | 50 | // Generate secure password consistent with password policy 51 | const password = passwordGenerator.generate({ 52 | length: 20, 53 | numbers: true, 54 | symbols: true, 55 | strict: true, 56 | }); 57 | 58 | // Check if password passes password policy 59 | const isValidPassword = validatePassword(password); 60 | if (!isValidPassword) { 61 | console.log("Failed to generate valid password"); 62 | return createError({ 63 | statusCode: 500, 64 | statusMessage: "Server error", 65 | }); 66 | } 67 | 68 | // Hash password 69 | const errorOrHashedPassword = await hashPassword(password); 70 | if (errorOrHashedPassword instanceof H3Error) { 71 | console.log("Error hashing password"); 72 | return createError({ 73 | statusCode: 500, 74 | statusMessage: "Server error", 75 | }); 76 | } 77 | 78 | const hashedPassword = errorOrHashedPassword as string; 79 | 80 | // Update database 81 | await prisma.users 82 | .update({ 83 | where: { 84 | uuid: uuid, 85 | }, 86 | data: { 87 | password: hashedPassword, 88 | }, 89 | }) 90 | .catch(async (e) => { 91 | console.error(e); 92 | error = e; 93 | }); 94 | 95 | // Check for database errors 96 | if (error) { 97 | console.log("Error updating user password"); 98 | return createError({ 99 | statusCode: 500, 100 | statusMessage: "Server error", 101 | }); 102 | } 103 | 104 | console.log("Updated user password"); 105 | return password; 106 | } 107 | 108 | /** 109 | * @desc Verifies password against a hash 110 | * @param hash Hashed password 111 | * @param password Unhashed password 112 | */ 113 | export async function verifyPassword( 114 | hash: string, 115 | password: string 116 | ): Promise { 117 | try { 118 | if (await argon2.verify(hash, password)) { 119 | return true; 120 | } else { 121 | return false; 122 | } 123 | } catch (err) { 124 | console.log(err); 125 | return false; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /iam/mvc/users/controller.ts: -------------------------------------------------------------------------------- 1 | /* Users controller 2 | * Routes all user requests 3 | */ 4 | 5 | import { index, create, show, permission, update, destroy } from "./model"; 6 | import { createRouter, defineEventHandler, useBase, H3Error } from 'h3'; 7 | import { canAccessAdmin } from "~~/iam/authz/permissions"; 8 | import { isOwner } from "~~/iam/authz/helpers"; 9 | import { validateCsrfToken } from "~~/iam/misc/utils/tokens"; 10 | 11 | // User not found error 12 | const userNotFoundError = createError({ 13 | statusCode: 401, 14 | statusMessage: "Unauthorized. User not found.", 15 | }); 16 | 17 | // Forbidden error 18 | const forbiddenError = createError({ 19 | statusCode: 403, 20 | statusMessage: "Forbidden", 21 | }); 22 | 23 | // Missing csrf token error 24 | const csrfTokenError = createError({ 25 | statusCode: 403, 26 | statusMessage: "Missing or invalid csrf token", 27 | }); 28 | 29 | const router = createRouter(); 30 | 31 | // Get all users 32 | router.get('/', defineEventHandler(async (event) => { 33 | if (!event.context.user) throw userNotFoundError 34 | if (!canAccessAdmin(event.context.user)) throw forbiddenError 35 | return await index(event) 36 | })); 37 | 38 | // Create a user 39 | router.post('/', defineEventHandler(async (event) => { 40 | return await create(event) 41 | })); 42 | 43 | // Get a single user 44 | router.get('/:uuid', defineEventHandler(async (event) => { 45 | // Get user uuid from request 46 | if (!event.context.user) throw userNotFoundError 47 | const uuid = event.context.params?.uuid 48 | 49 | // Permissions: to see record, user must be either super admin or be the owner 50 | if (uuid) 51 | if (event.context.user.uuid && !canAccessAdmin(event.context.user) && !isOwner(event.context.user.uuid, uuid)) 52 | throw forbiddenError; 53 | 54 | return await show(event) 55 | })); 56 | 57 | // Check if a user has a permission 58 | router.get('/:uuid/permission/:permission', defineEventHandler(async (event) => { 59 | if (!event.context.user) throw userNotFoundError 60 | if (!canAccessAdmin(event.context.user)) throw forbiddenError 61 | 62 | return await permission(event) 63 | })); 64 | 65 | // Edit a user 66 | router.put('/:uuid', defineEventHandler(async (event) => { 67 | // Check if csrf token is valid 68 | const tokenOrError = await validateCsrfToken(event); 69 | if (tokenOrError instanceof H3Error) throw csrfTokenError; 70 | 71 | // Get user uuid from request 72 | if (!event.context.user) throw userNotFoundError 73 | const uuid = event.context.params?.uuid 74 | 75 | // To edit record, user must be either super admin or be the owner 76 | if (uuid) 77 | if (event.context.user.uuid && !canAccessAdmin(event.context.user) && !isOwner(event.context.user.uuid, uuid)) 78 | throw forbiddenError; 79 | 80 | return await update(event) 81 | })); 82 | 83 | // Delete a user 84 | router.delete('/:uuid', defineEventHandler(async (event) => { 85 | // Check if csrf token is valid 86 | const tokenOrError = await validateCsrfToken(event); 87 | if (tokenOrError instanceof H3Error) throw csrfTokenError; 88 | 89 | // Get user uuid from request 90 | if (!event.context.user) throw userNotFoundError 91 | const uuid = event.context.params?.uuid 92 | 93 | // To delete record, user must be either super admin or be the owner 94 | if (uuid) 95 | if (event.context.user.uuid && !canAccessAdmin(event.context.user) && !isOwner(event.context.user.uuid, uuid)) 96 | throw forbiddenError; 97 | 98 | return await destroy(event) 99 | })); 100 | 101 | export default useBase('/api/iam/users', router.handler); 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /components/iamLogin.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 125 | 126 | 143 | -------------------------------------------------------------------------------- /components/iamRefreshTokensTable.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 138 | -------------------------------------------------------------------------------- /iam/mvc/refresh-tokens/queries.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { RefreshTokens } from "~~/iam/misc/types"; 3 | import { H3Event, H3Error } from "h3"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | /** 8 | * @desc Gets all users 9 | * @param event H3Event 10 | */ 11 | export async function getAllRefreshTokens( 12 | event: H3Event 13 | ): Promise { 14 | let refreshTokens = [] as RefreshTokens; 15 | let error = null; 16 | 17 | // Get query parameters from url 18 | const queryParams = getQuery(event) 19 | 20 | // Pagination variables 21 | let skip = queryParams.skip as string; 22 | let take = queryParams.take as string; 23 | 24 | await prisma.refresh_tokens 25 | .findMany({ 26 | skip: Number.isInteger(skip) ? parseInt(skip) : 0, 27 | take: Number.isInteger(take) ? parseInt(take) : 100, 28 | }) 29 | .then(async (result) => { 30 | refreshTokens = result; 31 | }) 32 | .catch(async (e) => { 33 | console.error(e); 34 | error = e; 35 | }); 36 | 37 | // Return error or tokens 38 | if (error) { 39 | console.log("Error retrieving refresh tokens"); 40 | return createError({ 41 | statusCode: 500, 42 | statusMessage: "Server error", 43 | }); 44 | } 45 | 46 | return refreshTokens; 47 | } 48 | 49 | /** 50 | * @desc Removes a paticular refresh token from database 51 | * @param event H3Event 52 | */ 53 | export async function destroyRefreshToken( 54 | event: H3Event 55 | ): Promise { 56 | // Get id from route 57 | const id = event.context.params?.id; 58 | 59 | if (!id) { 60 | console.log("Refresh token id is missing for delete"); 61 | return createError({ 62 | statusCode: 400, 63 | statusMessage: "Refresh token id is missing for delete", 64 | }); 65 | } 66 | 67 | let token = null; 68 | let error = null; 69 | 70 | await prisma.refresh_tokens 71 | .delete({ 72 | where: { 73 | id: parseInt(id), 74 | }, 75 | }) 76 | .then(async (result) => { 77 | token = result; 78 | }) 79 | .catch(async (e) => { 80 | console.error(e); 81 | error = e; 82 | }); 83 | 84 | // If we encounter an error, return error 85 | if (error) { 86 | console.log("Error deleting refresh token"); 87 | return createError({ 88 | statusCode: 500, 89 | statusMessage: "Server error", 90 | }); 91 | } 92 | 93 | // If we have a token, return the boolean 94 | if (token) return true; 95 | // otherwise return false (which shouldn't happen) 96 | else { 97 | console.log("Should not return false here"); 98 | return false; 99 | } 100 | } 101 | 102 | /** 103 | * @desc Removes all refresh tokens from database 104 | * @param event H3Event 105 | */ 106 | export async function destroyRefreshTokens( 107 | event: H3Event 108 | ): Promise { 109 | // Get id from route 110 | 111 | let tokens = null; 112 | let error = null; 113 | 114 | await prisma.refresh_tokens 115 | .deleteMany({ 116 | where: { 117 | id: { 118 | gt: 0, 119 | }, 120 | }, 121 | }) 122 | .then(async (result) => { 123 | tokens = result; 124 | }) 125 | .catch(async (e) => { 126 | console.error(e); 127 | error = e; 128 | }); 129 | 130 | // If we encounter an error, return error 131 | if (error) { 132 | console.log("Error deleting refresh tokens"); 133 | return createError({ 134 | statusCode: 500, 135 | statusMessage: "Server error", 136 | }); 137 | } 138 | 139 | // If we have a user, return the boolean 140 | if (tokens) { 141 | console.log(tokens); 142 | return true; 143 | } 144 | // otherwise return false (which shouldn't happen) 145 | else { 146 | console.log("We should not return false here"); 147 | return false; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /composables/useIamAdmin.ts: -------------------------------------------------------------------------------- 1 | import { JSONResponse, User } from "~~/iam/misc/types"; 2 | 3 | // Composable to make user management tasks easier 4 | export default function useIamAdmin() { 5 | return { 6 | getUsers, 7 | createUser, 8 | updateUser, 9 | deleteUser, 10 | userHasPermission, 11 | getRefreshTokens, 12 | deleteRefreshToken, 13 | deleteRefreshTokens, 14 | }; 15 | } 16 | 17 | /** 18 | * @desc Get users 19 | * @returns {Promise} 20 | */ 21 | async function getUsers(): Promise { 22 | const response = await $fetch("/api/iam/users", { 23 | headers: { 24 | "client-platform": "browser", 25 | }, 26 | }); 27 | 28 | return response; 29 | } 30 | 31 | /** 32 | * @desc Create a user 33 | * @param user User to create 34 | * @returns {Promise} 35 | */ 36 | async function createUser(user: User): Promise { 37 | const response = await $fetch(`/api/iam/authn/register`, { 38 | method: "POST", 39 | headers: { 40 | "client-platform": "browser", 41 | }, 42 | body: user, 43 | }); 44 | 45 | return response; 46 | } 47 | 48 | /** 49 | * @desc Update a user 50 | * @param uuid User's uuid 51 | * @param values User record's editable values 52 | * @returns {Promise} 53 | */ 54 | async function updateUser(user: User): Promise { 55 | const response = await $fetch(`/api/iam/users/${user.uuid}`, { 56 | method: "PUT", 57 | headers: { 58 | "client-platform": "browser", 59 | }, 60 | body: user, 61 | }); 62 | 63 | console.log('Response: ', response) 64 | return response; 65 | } 66 | 67 | /** 68 | * @desc Delete a user 69 | * @uuid User uuid 70 | * @csrfToken Cross-site request forgery prevention token 71 | * @returns {Promise} 72 | */ 73 | async function deleteUser(user: User): Promise { 74 | const response = await $fetch(`/api/iam/users/${user.uuid}`, { 75 | method: "DELETE", 76 | headers: { 77 | "client-platform": "browser", 78 | }, 79 | body: { 80 | csrf_token: user.csrf_token, 81 | }, 82 | }); 83 | 84 | return response; 85 | } 86 | 87 | /** 88 | * @desc Check user permission 89 | * @returns {Promise} 90 | */ 91 | async function userHasPermission(user: User, permission: string): Promise { 92 | const response = await $fetch(`/api/iam/users/${user.uuid}/permission/${permission}`, { 93 | headers: { 94 | "client-platform": "browser", 95 | }, 96 | }); 97 | 98 | return response; 99 | } 100 | 101 | /** 102 | * @desc Get all refresh tokens 103 | * @returns {Promise} 104 | */ 105 | async function getRefreshTokens(): Promise { 106 | const response = await $fetch("/api/iam/refresh-tokens", { 107 | headers: { 108 | "client-platform": "browser", 109 | }, 110 | }); 111 | 112 | return response; 113 | } 114 | 115 | /** 116 | * @desc Delete a refresh token 117 | * @returns {Promise} 118 | */ 119 | async function deleteRefreshToken( 120 | id: number, 121 | csrfToken: string 122 | ): Promise { 123 | const response = await $fetch(`/api/iam/refresh-tokens/${id}`, { 124 | method: "DELETE", 125 | headers: { 126 | "client-platform": "browser", 127 | }, 128 | body: { 129 | csrf_token: csrfToken, 130 | }, 131 | }); 132 | 133 | return response; 134 | } 135 | 136 | /** 137 | * @desc Deletes all refresh token 138 | * @returns {Promise} 139 | */ 140 | async function deleteRefreshTokens(csrfToken: string): Promise { 141 | const response = await $fetch(`/api/iam/refresh-tokens/`, { 142 | method: "DELETE", 143 | headers: { 144 | "client-platform": "browser", 145 | }, 146 | body: { 147 | csrf_token: csrfToken, 148 | }, 149 | }); 150 | 151 | return response; 152 | } 153 | -------------------------------------------------------------------------------- /pages/iam/dashboard/profile.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 145 | 146 | 151 | -------------------------------------------------------------------------------- /components/iamRegister.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 133 | 134 | 147 | -------------------------------------------------------------------------------- /components/NxAlert.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 100 | 101 | 178 | -------------------------------------------------------------------------------- /pages/iam/docs/frontend.vue: -------------------------------------------------------------------------------- 1 | 115 | -------------------------------------------------------------------------------- /components/NxForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 106 | 107 | -------------------------------------------------------------------------------- /pages/iam/index.vue: -------------------------------------------------------------------------------- 1 | 125 | 126 | 131 | 132 | 144 | -------------------------------------------------------------------------------- /pages/iam/dashboard/settings.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 162 | 163 | 175 | -------------------------------------------------------------------------------- /components/iamDashboard.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 153 | 154 | 167 | -------------------------------------------------------------------------------- /iam/mvc/users/queries.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { validateUserUpdate, validateUserDelete } from "~~/iam/misc/utils/validators"; 3 | import { User } from "~~/iam/misc/types"; 4 | import { H3Event, H3Error } from "h3"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | /** 9 | * @desc Gets all users 10 | * @param event H3Event 11 | */ 12 | export async function getAllUsers( 13 | event: H3Event 14 | ): Promise | H3Error> { 15 | let users = [] as Array; 16 | let error = null; 17 | 18 | // Get query parameters from url 19 | const queryParams = getQuery(event) 20 | 21 | // Pagination variables 22 | let skip = queryParams.skip as string; 23 | let take = queryParams.take as string; 24 | 25 | await prisma.users 26 | .findMany({ 27 | skip: Number.isInteger(skip) ? parseInt(skip) : 0, 28 | take: Number.isInteger(take) ? parseInt(take) : 100, 29 | }) 30 | .then(async (result) => { 31 | users = result; 32 | }) 33 | .catch(async (e) => { 34 | console.error(e); 35 | error = e; 36 | }); 37 | 38 | // Return error or users 39 | if (error) { 40 | console.log("Error retrieving users"); 41 | return createError({ 42 | statusCode: 500, 43 | statusMessage: "Server error", 44 | }); 45 | } 46 | 47 | return users; 48 | } 49 | 50 | /** 51 | * @desc Gets one user 52 | * @param event H3Event 53 | */ 54 | export async function showUser(event: H3Event): Promise { 55 | const uuid = event.context.params?.uuid; 56 | 57 | if (!uuid) { 58 | console.log('Missing user uuid') 59 | return createError({ 60 | statusCode: 400, 61 | statusMessage: "Missing user uuid", 62 | }); 63 | } 64 | 65 | let error = null; 66 | let user = {} as User | null; 67 | 68 | await prisma.users 69 | .findUnique({ 70 | where: { 71 | uuid: uuid, 72 | }, 73 | }) 74 | .then(async (result) => { 75 | user = result; 76 | }) 77 | .catch(async (e) => { 78 | console.error(e); 79 | error = e; 80 | }); 81 | 82 | // If error, return error 83 | if (error) { 84 | console.log("Error getting one user"); 85 | return createError({ 86 | statusCode: 500, 87 | statusMessage: "Server error", 88 | }); 89 | } 90 | 91 | // Prisma returns empty object if user not found, so check if user has email 92 | if (user && "email" in user === false) { 93 | return createError({ 94 | statusCode: 404, 95 | statusMessage: "User not found", 96 | }); 97 | } 98 | 99 | // Because Prisma can return null for user, we have to check for null before returning user 100 | if (user === null) 101 | return createError({ 102 | statusCode: 404, 103 | statusMessage: "User not found", 104 | }); 105 | 106 | return user; 107 | } 108 | 109 | /** 110 | * @desc Update a user 111 | * @param event H3Event 112 | */ 113 | export async function updateUser(event: H3Event): Promise { 114 | const errorOrVoid = await validateUserUpdate(event); 115 | if (errorOrVoid instanceof H3Error) return errorOrVoid; 116 | 117 | // Get parameters 118 | const body = await readBody(event); 119 | 120 | // Get uuid from event context 121 | const uuid = event.context.params?.uuid; 122 | 123 | if (!uuid) { 124 | console.log('Missing user uuid') 125 | return createError({ 126 | statusCode: 400, 127 | statusMessage: "Missing user uuid", 128 | }); 129 | } 130 | 131 | let user = {} as User; 132 | let error = null; 133 | 134 | // Remove uuid and id from body so they cannot be updated accidentally 135 | delete body.uuid 136 | delete body.id 137 | 138 | // Remove csrf_token because it's not in user schema 139 | delete body.csrf_token 140 | 141 | await prisma.users 142 | .update({ 143 | where: { 144 | uuid: uuid, 145 | }, 146 | data: body, 147 | }) 148 | .then(async (response) => { 149 | user = response; 150 | }) 151 | .catch(async (e) => { 152 | console.error(e); 153 | error = e; 154 | }); 155 | 156 | // If error, return error 157 | if (error) 158 | return createError({ 159 | statusCode: 500, 160 | statusMessage: "Server error", 161 | }); 162 | 163 | return user; 164 | } 165 | 166 | /** 167 | * @desc Removes user from database 168 | * @param event H3Event 169 | */ 170 | export async function destroyUser(event: H3Event): Promise { 171 | console.log('Got in destroyUser') 172 | const errorOrVoid = await validateUserDelete(event); 173 | if (errorOrVoid instanceof H3Error) return errorOrVoid; 174 | 175 | const uuid = event.context.params?.uuid; 176 | 177 | if (!uuid) { 178 | console.log('Missing user uuid') 179 | return createError({ 180 | statusCode: 400, 181 | statusMessage: "Missing user uuid", 182 | }); 183 | } 184 | 185 | let user = {} as User; 186 | let error = null; 187 | 188 | await prisma.users 189 | .delete({ 190 | where: { 191 | uuid: uuid, 192 | }, 193 | }) 194 | .then(async (result) => { 195 | user = result; 196 | }) 197 | .catch(async (e) => { 198 | console.error(e); 199 | error = e; 200 | }); 201 | 202 | // If we encounter an error, return error 203 | if (error) { 204 | console.log("Error deleting user"); 205 | return createError({ 206 | statusCode: 500, 207 | statusMessage: "Server error", 208 | }); 209 | } 210 | 211 | // If we have a user, return the boolean 212 | if (user) return true; 213 | // otherwise return false (which shouldn't happen) 214 | else return false; 215 | } 216 | -------------------------------------------------------------------------------- /components/IamDashboardHeader.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 203 | 204 | 230 | -------------------------------------------------------------------------------- /iam/mvc/users/model.ts: -------------------------------------------------------------------------------- 1 | import { H3Event, H3Error } from "h3"; 2 | import { getAllUsers, showUser, updateUser, destroyUser } from "./queries"; 3 | import { JSONResponse, User, Session } from "~~/iam/misc/types"; 4 | import { getUserSession } from "~~/iam/misc/utils/sessions"; 5 | import { hasPermission } from "~~/iam/authz/permissions"; 6 | 7 | /** 8 | * @desc Shows all users 9 | * @param event H3 Event passed from api 10 | * @returns {Promise} 11 | */ 12 | export async function index(event: H3Event): Promise { 13 | const response = {} as JSONResponse; 14 | const errorOrUsers = await getAllUsers(event); 15 | let sessionOrError = {} as Session | H3Error; 16 | 17 | // If error, return error 18 | if (errorOrUsers instanceof H3Error) { 19 | response.status = "fail"; 20 | response.error = errorOrUsers; 21 | return response; 22 | } 23 | 24 | // Get csrf token from using session id token 25 | const sessionId = getCookie(event, "iam-sid"); 26 | if (sessionId) sessionOrError = await getUserSession(sessionId); 27 | 28 | // If error, return error 29 | if (sessionOrError instanceof H3Error) { 30 | console.log("Error getting user session"); 31 | response.status = "fail"; 32 | response.error = response.error = createError({ 33 | statusCode: 500, 34 | statusMessage: "Server error", 35 | }); 36 | } 37 | 38 | // Otherwise get session and csrf token 39 | const session = sessionOrError as Session; 40 | 41 | // Otherwise, return users, and add token 42 | const users = errorOrUsers as Array; 43 | response.status = "success"; 44 | response.data = { 45 | users, 46 | csrf_token: session.csrf_token, 47 | }; 48 | 49 | return response; 50 | } 51 | 52 | /** 53 | * @desc Creates a new user in database 54 | * @param event H3 Event passed from api 55 | * @returns {Promise} 56 | */ 57 | export async function create(event: H3Event): Promise { 58 | // Return error because all users will be created from /authn/register endpoint 59 | const response = {} as JSONResponse; 60 | response.status = "fail"; 61 | response.error = createError({ 62 | statusCode: 422, 63 | statusMessage: "All users must be created from authn/register endpoint", 64 | }); 65 | 66 | return response; 67 | } 68 | 69 | /** 70 | * @desc Show a particular user 71 | * @param event H3 Event passed from api 72 | * @returns {Promise} 73 | */ 74 | export async function show(event: H3Event): Promise { 75 | const response = {} as JSONResponse; 76 | const errorOrUser = await showUser(event); 77 | 78 | // If error, return error 79 | if (errorOrUser instanceof H3Error) { 80 | response.status = "fail"; 81 | response.error = errorOrUser; 82 | return response; 83 | } 84 | 85 | // Otherwise, return user 86 | const user = errorOrUser as User; 87 | response.status = "success"; 88 | response.data = user; 89 | 90 | return response; 91 | } 92 | 93 | /** 94 | * @desc Check if a user has a permission 95 | * @param event H3 Event passed from api 96 | * @returns {Promise} 97 | */ 98 | export async function permission(event: H3Event): Promise { 99 | const response = {} as JSONResponse; 100 | 101 | // Get user and permission 102 | const user = event.context.user as User 103 | const permission = event.context.params?.permission 104 | 105 | // If no user, return error 106 | if (!user) { 107 | console.log('Failed to get user for permission check.') 108 | response.status = "fail" 109 | response.error = createError({ 110 | statusCode: 401, 111 | statusMessage: "Failed to get user.", 112 | }); 113 | return response 114 | } 115 | 116 | // If no permission given, return error 117 | if (!permission) { 118 | console.log('No permission given to check if user has permission') 119 | response.status = "fail" 120 | response.error = createError({ 121 | statusCode: 400, 122 | statusMessage: "No permission given", 123 | }); 124 | return response 125 | } 126 | 127 | const userHasPermission = hasPermission(user, permission); 128 | 129 | // If no user, return error 130 | if (!userHasPermission) { 131 | console.log(`User: ${user.uuid} does NOT have permission: ${permission}`); 132 | response.status = "fail" 133 | response.error = createError({ 134 | statusCode: 401, 135 | statusMessage: `User: ${user.uuid} does NOT have permission: ${permission}`, 136 | }); 137 | return response 138 | } else { 139 | response.status = "success" 140 | response.data = `User: ${user.uuid} has permission: ${permission}` 141 | return response 142 | } 143 | } 144 | 145 | /** 146 | * @desc Update particular user 147 | * @param event H3 Event passed from api 148 | * @returns {Promise} 149 | */ 150 | export async function update(event: H3Event): Promise { 151 | const response = {} as JSONResponse; 152 | const errorOrUser = await updateUser(event); 153 | 154 | // If error, return error 155 | if (errorOrUser instanceof H3Error) { 156 | response.status = "fail"; 157 | response.error = errorOrUser; 158 | return response; 159 | } 160 | 161 | // Otherwise, return user 162 | const user = errorOrUser as User; 163 | response.status = "success"; 164 | response.data = user; 165 | 166 | return response; 167 | } 168 | 169 | /** 170 | * @desc Delete a particular user 171 | * @param event H3 Event passed from api 172 | * @returns {Promise} 173 | */ 174 | export async function destroy(event: H3Event): Promise { 175 | const response = {} as JSONResponse; 176 | const errorOrBoolean = await destroyUser(event); 177 | 178 | // If error, return error 179 | if (errorOrBoolean instanceof H3Error) { 180 | response.status = "fail"; 181 | response.error = errorOrBoolean; 182 | return response; 183 | } 184 | 185 | // If false is returned, which shouldn't happen 186 | if (errorOrBoolean === false) { 187 | response.status = "fail"; 188 | return response; 189 | } 190 | // Otherwise user successfully deleted 191 | else { 192 | response.status = "success"; 193 | return response; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /composables/useIam.ts: -------------------------------------------------------------------------------- 1 | import { JSONResponse, User } from "~~/iam/misc/types"; 2 | 3 | // Composable to make authentication tasks easier 4 | export default function useIam() { 5 | return { 6 | register, 7 | login, 8 | logout, 9 | isAuthenticated, 10 | refresh, 11 | getProfile, 12 | loginWithGoogle, 13 | updateProfile, 14 | deleteAccount, 15 | resetPassword, 16 | verifyReset, 17 | verifyEmail, 18 | verifyEmailToken, 19 | }; 20 | } 21 | 22 | /** 23 | * @desc Register new user 24 | * @param user User to register 25 | * @returns {Promise} 26 | */ 27 | async function register(user: User): Promise { 28 | // Attempt register 29 | const response = await $fetch("/api/iam/authn/register", { 30 | method: "POST", 31 | headers: { 32 | "client-platform": "browser", 33 | }, 34 | body: user, 35 | }); 36 | 37 | return response; 38 | } 39 | 40 | /** 41 | * @desc Register new user 42 | * @param user User to log in 43 | * @returns {Promise} 44 | */ 45 | async function login(user: User): Promise { 46 | const response = await $fetch("/api/iam/authn/login", { 47 | method: "POST", 48 | headers: { 49 | "client-platform": "browser", 50 | }, 51 | body: user, 52 | }); 53 | 54 | return response; 55 | } 56 | 57 | /** 58 | * @desc Update user profile 59 | * @returns {Promise} 60 | */ 61 | 62 | async function updateProfile(user: User): Promise { 63 | const response = await $fetch("/api/iam/authn/update", { 64 | method: "PUT", 65 | headers: { 66 | "client-platform": "browser", 67 | }, 68 | body: user, 69 | }); 70 | 71 | return response; 72 | } 73 | 74 | /** 75 | * @desc Get user profile 76 | * @returns {Promise} 77 | */ 78 | async function getProfile(): Promise { 79 | const response = await $fetch("/api/iam/authn/profile", { 80 | headers: { 81 | "client-platform": "browser", 82 | }, 83 | }); 84 | 85 | return response; 86 | } 87 | 88 | /** 89 | * @desc Attempt to log user out 90 | * @returns {Promise} 91 | */ 92 | async function logout(): Promise { 93 | const response = await $fetch("/api/iam/authn/logout", { 94 | method: "POST", 95 | headers: { 96 | "client-platform": "browser", 97 | }, 98 | }); 99 | 100 | return response; 101 | } 102 | 103 | /** 104 | * @desc Receives user token from Google login, and signs user 105 | * @param token Access token received from Google after login 106 | * @returns {Promise} 107 | */ 108 | async function loginWithGoogle(token: string): Promise { 109 | const response = await $fetch("/api/iam/authn/login-google", { 110 | method: "POST", 111 | headers: { 112 | "client-platform": "browser", 113 | }, 114 | body: { 115 | token: token, 116 | }, 117 | }); 118 | 119 | return response; 120 | } 121 | 122 | /** 123 | * @desc Returns true/false depending on whether the user is logged in or not 124 | * @returns {Promise} 125 | */ 126 | async function isAuthenticated(): Promise { 127 | // Api response always has status, data, or error 128 | const { status } = await $fetch("/api/iam/authn/isauthenticated", { 129 | headers: { 130 | "client-platform": "browser", 131 | }, 132 | }); 133 | 134 | // If status is success, then user is authenticated, and return true, otherwise return false 135 | return status === "success"; 136 | } 137 | 138 | /** 139 | * @desc Attempts to refresh tokens 140 | * @returns {Promise} 141 | */ 142 | async function refresh(): Promise { 143 | const response = await $fetch("/api/iam/authn/refresh", { 144 | method: "POST", 145 | headers: { 146 | "client-platform": "browser", 147 | }, 148 | }); 149 | 150 | return response; 151 | } 152 | 153 | /** 154 | * @desc Delete user account 155 | * @returns {Promise} 156 | */ 157 | async function deleteAccount( 158 | uuid: string, 159 | csrfToken: string 160 | ): Promise { 161 | const response = await $fetch("/api/iam/authn/delete", { 162 | method: "DELETE", 163 | headers: { 164 | "client-platform": "browser", 165 | }, 166 | body: { 167 | uuid: uuid, 168 | csrf_token: csrfToken, 169 | }, 170 | }); 171 | 172 | return response; 173 | } 174 | 175 | /** 176 | * @desc Reset user's password 177 | * @returns {Promise} 178 | */ 179 | async function resetPassword(email: string): Promise { 180 | const response = await $fetch("/api/iam/authn/reset", { 181 | method: "POST", 182 | headers: { 183 | "client-platform": "browser", 184 | }, 185 | body: { 186 | email: email, 187 | }, 188 | }); 189 | 190 | return response; 191 | } 192 | 193 | /** 194 | * @desc Verify reset password token sent by user 195 | * @returns {Promise} 196 | */ 197 | async function verifyReset(token: string): Promise { 198 | const response = await $fetch("/api/iam/authn/verifyreset", { 199 | method: "POST", 200 | headers: { 201 | "client-platform": "browser", 202 | }, 203 | body: { 204 | token: token, 205 | }, 206 | }); 207 | 208 | return response; 209 | } 210 | 211 | /** 212 | * @desc Verify user email after registration 213 | * @returns {Promise} 214 | */ 215 | async function verifyEmail(email: string): Promise { 216 | const response = await $fetch("/api/iam/authn/verifyemail", { 217 | method: "POST", 218 | headers: { 219 | "client-platform": "browser", 220 | }, 221 | body: { 222 | email: email, 223 | }, 224 | }); 225 | 226 | return response; 227 | } 228 | 229 | /** 230 | * @desc Verify email verification token sent by user 231 | * @returns {Promise} 232 | */ 233 | async function verifyEmailToken(token: string): Promise { 234 | const response = await $fetch("/api/iam/authn/verifyemailtoken", { 235 | method: "POST", 236 | headers: { 237 | "client-platform": "browser", 238 | }, 239 | body: { 240 | token: token, 241 | }, 242 | }); 243 | 244 | return response; 245 | } 246 | -------------------------------------------------------------------------------- /iam/misc/utils/sessions.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { Session } from "~~/iam/misc/types"; 3 | import { H3Event, H3Error } from "h3"; 4 | import { makeRandomString32, makeUuid } from "./../utils/passwords"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | /** 9 | * @Desc Create user session 10 | * @param user_id User id 11 | * @returns {Promise} Returns error or the given uuid 12 | */ 13 | export async function createUserSession( 14 | userId: number, 15 | accessToken: string, 16 | event: H3Event 17 | ): Promise { 18 | let error = null; 19 | let session = null; 20 | 21 | // If no user id provided 22 | if (!userId) { 23 | console.log("User id not provided for create session"); 24 | return createError({ 25 | statusCode: 500, 26 | statusMessage: "Server error", 27 | }); 28 | } 29 | 30 | // If no access token provided 31 | if (!accessToken) { 32 | console.log("Access token not provided for create session"); 33 | return createError({ 34 | statusCode: 500, 35 | statusMessage: "Server error", 36 | }); 37 | } 38 | 39 | // If event not provided 40 | if (!event) { 41 | console.log("Event not provided for create session"); 42 | return createError({ 43 | statusCode: 500, 44 | statusMessage: "Server error", 45 | }); 46 | } 47 | 48 | const csrfToken = makeRandomString32(); 49 | const ipAddress = getRequestHeader(event, "x-forwarded-for"); 50 | 51 | // Create session 52 | await prisma.sessions 53 | .create({ 54 | data: { 55 | user_id: userId, 56 | sid: makeUuid(), 57 | start_time: new Date(), 58 | access_token: accessToken, 59 | csrf_token: csrfToken, 60 | is_active: true, 61 | ip_address: ipAddress ? ipAddress : "unable to get IP address", 62 | }, 63 | }) 64 | .then(async (result) => { 65 | session = result as Session; 66 | }) 67 | .catch(async (e) => { 68 | console.error(e); 69 | error = e; 70 | }); 71 | 72 | // Check for database errors 73 | if (error) { 74 | console.log("Error creating user session"); 75 | return createError({ 76 | statusCode: 500, 77 | statusMessage: "Server error", 78 | }); 79 | } 80 | 81 | // If we have a session, return it 82 | if (session) return session; 83 | 84 | // Otherwise, return an error 85 | console.log("We should not be getting this session error"); 86 | return createError({ 87 | statusCode: 500, 88 | statusMessage: "Server error", 89 | }); 90 | } 91 | 92 | /** 93 | * @Desc Returns session given session id 94 | * @param sessionId Session id 95 | * @returns {Promise} Returns error or the given uuid 96 | */ 97 | export async function getUserSession( 98 | sessionId: string 99 | ): Promise { 100 | let error = null; 101 | let session = null; 102 | 103 | // Create session 104 | await prisma.sessions 105 | .findUnique({ 106 | where: { 107 | sid: sessionId, 108 | }, 109 | }) 110 | .then(async (result) => { 111 | session = result; 112 | }) 113 | .catch(async (e) => { 114 | console.error(e); 115 | error = e; 116 | }); 117 | 118 | // Check for database errors 119 | if (error) { 120 | console.log("Error retrieving user session"); 121 | return createError({ 122 | statusCode: 500, 123 | statusMessage: "Server error", 124 | }); 125 | } 126 | 127 | // If we have a session, return it 128 | if (session) return session; 129 | 130 | // Otherwise, return an error 131 | console.log("We should not be getting this retrieve session error"); 132 | return createError({ 133 | statusCode: 500, 134 | statusMessage: "Server error", 135 | }); 136 | } 137 | 138 | /** 139 | * @Desc Deactivates all of a user's sessions 140 | * @param userId User id 141 | * @returns {Promise} Returns error or the given uuid 142 | */ 143 | export async function deactivateUserSessions( 144 | userId: number 145 | ): Promise { 146 | let error = null; 147 | let session = null; 148 | 149 | // Deactivate session 150 | await prisma.sessions 151 | .updateMany({ 152 | where: { 153 | user_id: userId, 154 | }, 155 | data: { 156 | is_active: false, 157 | }, 158 | }) 159 | .then(async (result) => { 160 | session = result; 161 | }) 162 | .catch(async (e) => { 163 | console.error(e); 164 | error = e; 165 | }); 166 | 167 | // Check for database errors 168 | if (error) { 169 | console.log("Error deactivating user session"); 170 | return createError({ 171 | statusCode: 500, 172 | statusMessage: "Server error", 173 | }); 174 | } 175 | 176 | // If we have a session, return it 177 | if (session) return session; 178 | 179 | // Otherwise, return an error 180 | console.log("We should not be getting this deactivate user session error"); 181 | return createError({ 182 | statusCode: 500, 183 | statusMessage: "Server error", 184 | }); 185 | } 186 | 187 | /** 188 | * @Desc Records end time of a user session 189 | * @param sessionId Session id 190 | * @returns {Promise} Returns error or the given uuid 191 | */ 192 | export async function endUserSession( 193 | sessionId: string 194 | ): Promise { 195 | let error = null; 196 | let session = null; 197 | 198 | // Deactivate session 199 | await prisma.sessions 200 | .update({ 201 | where: { 202 | sid: sessionId, 203 | }, 204 | data: { 205 | end_time: new Date(), 206 | }, 207 | }) 208 | .then(async (result) => { 209 | session = result; 210 | }) 211 | .catch(async (e) => { 212 | console.error(e); 213 | error = e; 214 | }); 215 | 216 | // Check for database errors 217 | if (error) { 218 | console.log("Error ending user session"); 219 | return createError({ 220 | statusCode: 500, 221 | statusMessage: "Server error", 222 | }); 223 | } 224 | 225 | // If we have a session, return it 226 | if (session) return session; 227 | 228 | // Otherwise, return an error 229 | console.log("We should not be getting this update user session error"); 230 | return createError({ 231 | statusCode: 500, 232 | statusMessage: "Server error", 233 | }); 234 | } 235 | 236 | 237 | -------------------------------------------------------------------------------- /components/NxNavbar.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 161 | 162 | 330 | -------------------------------------------------------------------------------- /pages/iam/docs/concepts.vue: -------------------------------------------------------------------------------- 1 | 162 | -------------------------------------------------------------------------------- /components/iamUsersTable.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 314 | 315 | 320 | -------------------------------------------------------------------------------- /iam/misc/utils/logins.ts: -------------------------------------------------------------------------------- 1 | import { User, TokensSession, Session } from "~~/iam/misc/types"; 2 | import jwt from "jsonwebtoken"; 3 | import { H3Event, H3Error } from "h3"; 4 | import { OAuth2Client } from "google-auth-library"; 5 | import { makeUuid, verifyPassword } from "./passwords"; 6 | import { getUserByEmail, getUserById, updateLastLogin } from "./users"; 7 | import { storeRefreshToken, deactivateRefreshTokens, } from "./tokens"; 8 | import { createUserSession, deactivateUserSessions, getUserSession, endUserSession } from "./sessions"; 9 | 10 | const config = useRuntimeConfig(); 11 | 12 | /** 13 | * @desc Authenticates user 14 | * @param event Event from Api 15 | */ 16 | export async function login(event: H3Event): Promise { 17 | const tokens = {} as TokensSession; 18 | const body = await readBody(event); 19 | 20 | if (!body) 21 | return createError({ 22 | statusCode: 401, 23 | statusMessage: "No email or password provided", 24 | }); 25 | 26 | const user = await getUserByEmail(body.email); 27 | 28 | if (user === null) { 29 | return createError({ statusCode: 401, statusMessage: "Invalid login" }); 30 | } 31 | 32 | if (await verifyPassword(user.password, body.password)) { 33 | // Update last login time 34 | await updateLastLogin(user.email); 35 | 36 | const publicUser = { 37 | uuid: user.uuid, 38 | email: user.email, 39 | }; 40 | 41 | // Create access token 42 | const accessToken = jwt.sign(publicUser, config.iamAccessTokenSecret, { 43 | expiresIn: "15m", 44 | issuer: "NuxtIam", 45 | jwtid: makeUuid(), 46 | }); 47 | 48 | // Create refresh token 49 | const tokenId = makeUuid(); 50 | const refreshToken = jwt.sign(publicUser, config.iamRefreshTokenSecret, { 51 | expiresIn: "14d", 52 | issuer: "NuxtIam", 53 | jwtid: tokenId, 54 | }); 55 | 56 | // Deactivate any other tokens 57 | const deactivateTokenError = await deactivateRefreshTokens(user.id); 58 | if (deactivateTokenError) return deactivateTokenError; 59 | 60 | // Store tokens 61 | const storeTokenError = await storeRefreshToken(tokenId, user.id); 62 | if (storeTokenError) return storeTokenError; 63 | 64 | // Assign tokens 65 | tokens.accessToken = accessToken; 66 | tokens.refreshToken = refreshToken; 67 | 68 | // Create user session, if error, return error 69 | const sessionOrTokenError = await createUserSession( 70 | user.id, 71 | accessToken, 72 | event 73 | ); 74 | 75 | // If session error, return error 76 | if (sessionOrTokenError instanceof H3Error) { 77 | console.log("Trouble creating session"); 78 | return createError({ statusCode: 500, statusMessage: "Server error" }); 79 | } 80 | 81 | // Get session and session id 82 | const session = sessionOrTokenError as Session; 83 | tokens.sid = session.sid; 84 | 85 | return tokens; 86 | } 87 | 88 | return createError({ statusCode: 401, statusMessage: "Invalid login" }); 89 | } 90 | 91 | /** 92 | * @desc Logs a user out 93 | * @param event Event from Api 94 | */ 95 | export async function logout(event: H3Event): Promise { 96 | let sessionOrError = {} as H3Error | Session; 97 | 98 | // Get session id and session 99 | const sessionId = getCookie(event, "iam-sid"); 100 | if (sessionId) sessionOrError = await getUserSession(sessionId); 101 | 102 | // If error, log error but delete all cookies anyway 103 | if (sessionOrError instanceof H3Error) { 104 | console.log( 105 | "Error with logout. Sessions might not be disabled. Security risk." 106 | ); 107 | console.log("Proceeding with removing all cookies"); 108 | deleteCookie(event, "iam-access-token"); 109 | deleteCookie(event, "iam-refresh-token"); 110 | deleteCookie(event, "iam-sid"); 111 | } 112 | // Otherwise deactivate refresh tokens and all other user's sessions 113 | else { 114 | const session = sessionOrError as Session; 115 | const userOrNull = await getUserById(session.user_id); 116 | 117 | console.log("Cookies and session id removed."); 118 | deleteCookie(event, "iam-access-token"); 119 | deleteCookie(event, "iam-refresh-token"); 120 | deleteCookie(event, "iam-sid"); 121 | 122 | // If no user, log error, but delete all cookies anyway 123 | if (userOrNull === null) { 124 | console.log("Error with logout. User not found"); 125 | } else { 126 | // Otherwise get user 127 | const user = userOrNull as User; 128 | // Deactivate all refresh tokens 129 | const deactivateError = await deactivateRefreshTokens(user.id); 130 | if (deactivateError) { 131 | console.log(`Failed to deactivate user:${user.email}'s refresh tokens`); 132 | return createError({ 133 | statusCode: 500, 134 | statusMessage: "Logout error.", 135 | }); 136 | } 137 | 138 | // Deactivate user sessions 139 | const deactivateSessionsError = await deactivateUserSessions(user.id); 140 | if (deactivateSessionsError instanceof H3Error) 141 | return deactivateSessionsError; 142 | 143 | // End user session 144 | let endUserSessionOrError = {} as H3Error | Session; 145 | if (sessionId) endUserSessionOrError = await endUserSession(sessionId); 146 | 147 | // If error, log error 148 | if (endUserSessionOrError instanceof H3Error) { 149 | console.log("Error ending user session in logout. Security risk"); 150 | } 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * @Desc Get tokens after Google login 157 | * @param user Get Google 158 | * @param event H3 Event 159 | * @returns {Promise} Returns error or the given uuid 160 | */ 161 | export async function getTokensAfterGoogleLogin( 162 | user: User, 163 | event: H3Event 164 | ): Promise { 165 | const tokens = {} as TokensSession; 166 | 167 | if (user === null) { 168 | return createError({ 169 | statusCode: 401, 170 | statusMessage: "Invalid login. User not found.", 171 | }); 172 | } 173 | 174 | // Update last login time 175 | await updateLastLogin(user.email); 176 | 177 | const publicUser = { 178 | uuid: user.uuid, 179 | email: user.email, 180 | }; 181 | 182 | // Create access token 183 | const accessToken = jwt.sign(publicUser, config.iamAccessTokenSecret, { 184 | expiresIn: "15m", 185 | issuer: "NuxtIam", 186 | jwtid: makeUuid(), 187 | }); 188 | 189 | // Create refresh token 190 | const tokenId = makeUuid(); 191 | const refreshToken = jwt.sign(publicUser, config.iamRefreshTokenSecret, { 192 | expiresIn: "14d", 193 | issuer: "NuxtIam", 194 | jwtid: tokenId, 195 | }); 196 | 197 | // Deactivate any other tokens 198 | const deactivateTokenError = await deactivateRefreshTokens(user.id); 199 | if (deactivateTokenError) return deactivateTokenError; 200 | 201 | // Store tokens 202 | const storeTokenError = await storeRefreshToken(tokenId, user.id); 203 | if (storeTokenError) return storeTokenError; 204 | 205 | // Assign tokens 206 | tokens.accessToken = accessToken; 207 | tokens.refreshToken = refreshToken; 208 | 209 | // Create user session, if error, return error 210 | const sessionOrTokenError = await createUserSession( 211 | user.id, 212 | accessToken, 213 | event 214 | ); 215 | 216 | // If session error, return error 217 | if (sessionOrTokenError instanceof H3Error) { 218 | console.log("Trouble creating session"); 219 | return createError({ statusCode: 500, statusMessage: "Server error" }); 220 | } 221 | 222 | // Get session and session id 223 | const session = sessionOrTokenError as Session; 224 | tokens.sid = session.sid; 225 | 226 | return tokens; 227 | } 228 | 229 | /** 230 | * @Desc Verifies Google access token after sign in 231 | * @param token Google access token 232 | * @info Code obtained from https://developers.google.com/identity/gsi/web/guides/verify-google-id-token 233 | * @returns { H3Error|jwt.JwtPayload } Returns error or the given uuid 234 | */ 235 | export async function verifyGoogleToken( 236 | token: string 237 | ): Promise { 238 | let tokenPayload = null; 239 | 240 | const clientId = useRuntimeConfig().iamGoogleClientId; 241 | const client = new OAuth2Client(); 242 | async function verify() { 243 | const ticket = await client.verifyIdToken({ 244 | idToken: token, 245 | audience: clientId, // Specify the CLIENT_ID of the app that accesses the backend 246 | // Or, if multiple clients access the backend: 247 | //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3] 248 | }); 249 | tokenPayload = ticket.getPayload(); 250 | 251 | // if (payload) tokenPayload = payload["sub"]; 252 | // If request specified a G Suite domain: 253 | // const domain = payload['hd']; 254 | } 255 | await verify().catch(console.error); 256 | 257 | if (tokenPayload) return tokenPayload; 258 | else { 259 | console.log("Error verifying Google access token"); 260 | return createError({ statusCode: 401, statusMessage: "Unauthorized" }); 261 | } 262 | } 263 | 264 | /** 265 | * @desc Returns a list of all authenticated route based that need an authenticated user 266 | */ 267 | export function getAuthenticatedRoutes(): Array { 268 | return [ 269 | '/api/iam/users', 270 | '/api/iam/refresh-tokens', 271 | ]; 272 | } -------------------------------------------------------------------------------- /components/NxButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 230 | 231 | -------------------------------------------------------------------------------- /pages/iam/docs/features.vue: -------------------------------------------------------------------------------- 1 | 212 | --------------------------------------------------------------------------------