-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simple SSO feature for login flows from external services (#2553)
* Simple SSO feature for login flows from external services * linting
- Loading branch information
1 parent
3fe59a7
commit 2c9cb28
Showing
13 changed files
with
425 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React, { useEffect, useState } from "react"; | ||
import { FullScreenLoader } from "@/components/Preloader"; | ||
import { Navigate } from "react-router-dom"; | ||
import paths from "@/utils/paths"; | ||
import useQuery from "@/hooks/useQuery"; | ||
import System from "@/models/system"; | ||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants"; | ||
|
||
export default function SimpleSSOPassthrough() { | ||
const query = useQuery(); | ||
const redirectPath = query.get("redirectTo") || paths.home(); | ||
const [ready, setReady] = useState(false); | ||
const [error, setError] = useState(null); | ||
|
||
useEffect(() => { | ||
try { | ||
if (!query.get("token")) throw new Error("No token provided."); | ||
|
||
// Clear any existing auth data | ||
window.localStorage.removeItem(AUTH_USER); | ||
window.localStorage.removeItem(AUTH_TOKEN); | ||
window.localStorage.removeItem(AUTH_TIMESTAMP); | ||
|
||
System.simpleSSOLogin(query.get("token")) | ||
.then((res) => { | ||
if (!res.valid) throw new Error(res.message); | ||
|
||
window.localStorage.setItem(AUTH_USER, JSON.stringify(res.user)); | ||
window.localStorage.setItem(AUTH_TOKEN, res.token); | ||
window.localStorage.setItem(AUTH_TIMESTAMP, Number(new Date())); | ||
setReady(res.valid); | ||
}) | ||
.catch((e) => { | ||
setError(e.message); | ||
}); | ||
} catch (e) { | ||
setError(e.message); | ||
} | ||
}, []); | ||
|
||
if (error) | ||
return ( | ||
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center flex-col gap-4"> | ||
<p className="text-white font-mono text-lg">{error}</p> | ||
<p className="text-white/80 font-mono text-sm"> | ||
Please contact the system administrator about this error. | ||
</p> | ||
</div> | ||
); | ||
if (ready) return <Navigate to={redirectPath} />; | ||
|
||
// Loading state by default | ||
return <FullScreenLoader />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
const { makeJWT } = require("../utils/http"); | ||
const prisma = require("../utils/prisma"); | ||
|
||
/** | ||
* Temporary auth tokens are used for simple SSO. | ||
* They simply enable the ability for a time-based token to be used in the query of the /sso/login URL | ||
* to login as a user without the need of a username and password. These tokens are single-use and expire. | ||
*/ | ||
const TemporaryAuthToken = { | ||
expiry: 1000 * 60 * 6, // 1 hour | ||
tablename: "temporary_auth_tokens", | ||
writable: [], | ||
|
||
makeTempToken: () => { | ||
const uuidAPIKey = require("uuid-apikey"); | ||
return `allm-tat-${uuidAPIKey.create().apiKey}`; | ||
}, | ||
|
||
/** | ||
* Issues a temporary auth token for a user via its ID. | ||
* @param {number} userId | ||
* @returns {Promise<{token: string|null, error: string | null}>} | ||
*/ | ||
issue: async function (userId = null) { | ||
if (!userId) | ||
throw new Error("User ID is required to issue a temporary auth token."); | ||
await this.invalidateUserTokens(userId); | ||
|
||
try { | ||
const token = this.makeTempToken(); | ||
const expiresAt = new Date(Date.now() + this.expiry); | ||
await prisma.temporary_auth_tokens.create({ | ||
data: { | ||
token, | ||
expiresAt, | ||
userId: Number(userId), | ||
}, | ||
}); | ||
|
||
return { token, error: null }; | ||
} catch (error) { | ||
console.error("FAILED TO CREATE TEMPORARY AUTH TOKEN.", error.message); | ||
return { token: null, error: error.message }; | ||
} | ||
}, | ||
|
||
/** | ||
* Invalidates (deletes) all temporary auth tokens for a user via their ID. | ||
* @param {number} userId | ||
* @returns {Promise<boolean>} | ||
*/ | ||
invalidateUserTokens: async function (userId) { | ||
if (!userId) | ||
throw new Error( | ||
"User ID is required to invalidate temporary auth tokens." | ||
); | ||
await prisma.temporary_auth_tokens.deleteMany({ | ||
where: { userId: Number(userId) }, | ||
}); | ||
return true; | ||
}, | ||
|
||
/** | ||
* Validates a temporary auth token and returns the session token | ||
* to be set in the browser localStorage for authentication. | ||
* @param {string} publicToken - the token to validate against | ||
* @returns {Promise<{sessionToken: string|null, token: import("@prisma/client").temporary_auth_tokens & {user: import("@prisma/client").users} | null, error: string | null}>} | ||
*/ | ||
validate: async function (publicToken = "") { | ||
/** @type {import("@prisma/client").temporary_auth_tokens & {user: import("@prisma/client").users} | undefined | null} **/ | ||
let token; | ||
|
||
try { | ||
if (!publicToken) | ||
throw new Error( | ||
"Public token is required to validate a temporary auth token." | ||
); | ||
token = await prisma.temporary_auth_tokens.findUnique({ | ||
where: { token: String(publicToken) }, | ||
include: { user: true }, | ||
}); | ||
if (!token) throw new Error("Invalid token."); | ||
if (token.expiresAt < new Date()) throw new Error("Token expired."); | ||
if (token.user.suspended) throw new Error("User account suspended."); | ||
|
||
// Create a new session token for the user valid for 30 days | ||
const sessionToken = makeJWT( | ||
{ id: token.user.id, username: token.user.username }, | ||
"30d" | ||
); | ||
|
||
return { sessionToken, token, error: null }; | ||
} catch (error) { | ||
console.error("FAILED TO VALIDATE TEMPORARY AUTH TOKEN.", error.message); | ||
return { sessionToken: null, token: null, error: error.message }; | ||
} finally { | ||
// Delete the token after it has been used under all circumstances if it was retrieved | ||
if (token) | ||
await prisma.temporary_auth_tokens.delete({ where: { id: token.id } }); | ||
} | ||
}, | ||
}; | ||
|
||
module.exports = { TemporaryAuthToken }; |
12 changes: 12 additions & 0 deletions
12
server/prisma/migrations/20241029203722_init/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
-- CreateTable | ||
CREATE TABLE "temporary_auth_tokens" ( | ||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
"token" TEXT NOT NULL, | ||
"userId" INTEGER NOT NULL, | ||
"expiresAt" DATETIME NOT NULL, | ||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
CONSTRAINT "temporary_auth_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "temporary_auth_tokens_token_key" ON "temporary_auth_tokens"("token"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.