@unidir/unidir-nextjs 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var K=Object.create;var h=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var Y=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var G=(e,r)=>{for(var n in r)h(e,n,{get:r[n],enumerable:!0})},x=(e,r,n,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of z(r))!F.call(e,t)&&t!==n&&h(e,t,{get:()=>r[t],enumerable:!(a=M(r,t))||a.enumerable});return e};var B=(e,r,n)=>(n=e!=null?K(Y(e)):{},x(r||!e||!e.__esModule?h(n,"default",{value:e,enumerable:!0}):n,e)),Q=e=>x(h({},"__esModule",{value:!0}),e);var q={};G(q,{UserProvider:()=>A,initUniDir:()=>Z,useUser:()=>D});module.exports=Q(q);var N=require("cookie");var y=require("jose"),I=e=>new TextEncoder().encode(e.padEnd(32,"0").slice(0,32));async function P(e,r){return new y.EncryptJWT(e).setProtectedHeader({alg:"dir",enc:"A256GCM"}).setIssuedAt().setExpirationTime("24h").encrypt(I(r))}async function w(e,r){try{let{payload:n}=await(0,y.jwtDecrypt)(e,I(r));return n}catch{return null}}var u=require("next/server"),b=require("next/navigation");function T(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function C(e){let n=new TextEncoder().encode(e),a=await crypto.subtle.digest("SHA-256",n);return btoa(String.fromCharCode(...new Uint8Array(a))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var i=require("react");var _=require("jose");async function R(e,r,n={}){let a=(0,_.createRemoteJWKSet)(new URL(r)),{payload:t}=await(0,_.jwtVerify)(e,a,{issuer:n.issuer,audience:n.audience});return t}var E=require("react/jsx-runtime"),S=(0,i.createContext)({user:null,isLoading:!0,config:null});function A({children:e,config:r}){let[n,a]=(0,i.useState)(null),[t,s]=(0,i.useState)(!0),[p,o]=(0,i.useState)(null),[c,l]=(0,i.useState)(null),[k,d]=(0,i.useState)(null),[g,m]=(0,i.useState)(null),[U,v]=(0,i.useState)(null),[f,ee]=(0,i.useState)(null);return(0,i.useEffect)(()=>{async function L(){try{let O=await(await fetch("/api/auth/me")).json(),{companyId:J,domainId:W,email:V,email_verified:$,name:H}=await R(O.id_token,r.jwks||"https://oauth.igoodworks.com/jwks.json",{issuer:r.issuer||"http://oauth.unidir.igoodworks.com/",audience:r.clientId});a({companyId:J,domainId:W,email:V,email_verified:$,name:H})}finally{s(!1)}}L()},[]),(0,E.jsx)(S.Provider,{value:{user:n,isLoading:t,config:r},children:e})}var D=()=>(0,i.useContext)(S);var j=require("react/jsx-runtime"),X={login:"login",loginPath:"/api/auth/login",logout:"logout",callback:"callback",me:"me"};function Z(e,r){let n={...X,...r},a=async t=>{let s=t.headers.get("cookie")||"",o=(0,N.parse)(s).unidir_session;return o?await w(o,e.secret):null};return{handleAuth:()=>async t=>{let s=t.nextUrl.pathname.split("/").pop(),o=t.nextUrl.searchParams.get("device_id")||e.deviceId||"unknown-device";if(s===n.login){let c=t.nextUrl.searchParams.get("returnTo")||"/",l=T(),k=await C(l),d=new URL(`${e.domain}/authorize`);d.searchParams.set("client_id",e.clientId),d.searchParams.set("response_type","code"),d.searchParams.set("redirect_uri",e.redirectUri),d.searchParams.set("scope","openid profile email"),d.searchParams.set("code_challenge",k),d.searchParams.set("code_challenge_method","S256"),o&&d.searchParams.set("device_id",o);let g=u.NextResponse.redirect(d.toString());return g.cookies.set("unidir_pkce_verifier",l,{httpOnly:!0,secure:!0,sameSite:"lax",maxAge:300}),g.cookies.set("unidir_device_id",o,{httpOnly:!0,secure:!0,maxAge:600}),g.cookies.set("unidir_return_to",c,{httpOnly:!0,maxAge:300}),g}if(s===n.me){let c=await a(t);return c?u.NextResponse.json(c):new u.NextResponse(JSON.stringify({user:null}),{status:401})}if(s===n.logout){let c=u.NextResponse.redirect(new URL("/",t.url));return c.cookies.delete("unidir_session"),c}if(s==="callback"){let c=t.nextUrl.searchParams.get("code"),l=t.cookies.get("unidir_pkce_verifier")?.value;if(!c||!l)return new u.NextResponse("Missing code or verifier",{status:400});let d=t.cookies.get("unidir_device_id")?.value||o,m=await(await fetch(`${e.domain}/token`,{method:"POST",headers:{"Content-Type":"application/json","x-device-id":d},body:JSON.stringify({grant_type:"authorization_code",client_id:e.clientId,client_secret:e.clientSecret,code:c,code_verifier:l,redirect_uri:e.redirectUri,device_id:d})})).json();if(m.error)return u.NextResponse.json(m,{status:400});let U=await P(m,e.secret),v=t.cookies.get("unidir_return_to")?.value||"/",f=u.NextResponse.redirect(new URL(v,t.url));return f.cookies.delete("unidir_pkce_verifier"),f.cookies.set("unidir_session",U,{httpOnly:!0,secure:!0,sameSite:"lax",maxAge:3600*24}),f}return new u.NextResponse("Not Found",{status:404})},getSession:a,withPageAuthRequired:t=>async s=>{let{headers:p}=await import("next/headers"),o=await a({headers:await p()});return o||(0,b.redirect)(n.loginPath),(0,j.jsx)(t,{...s,user:o})},withMiddlewareAuth:()=>async t=>{let s=t.cookies.get("unidir_session")?.value;if(!(s?await w(s,e.secret):null)){let{pathname:o,search:c}=t.nextUrl,l=new URL(n.loginPath,t.url);return l.searchParams.set("returnTo",`${o}${c}`),u.NextResponse.redirect(l)}return u.NextResponse.next()}}}0&&(module.exports={UserProvider,initUniDir,useUser});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx","../src/session.ts","../src/pkce.ts","../src/client.tsx","../src/jwks.ts"],"sourcesContent":["import { parse } from \"cookie\";\r\nimport { encrypt, decrypt } from \"./session\";\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport { redirect } from \"next/navigation\";\r\nimport { generateCodeVerifier, generateCodeChallenge } from \"./pkce\";\r\n\r\nexport interface UniDirConfig {\r\n domain: string;\r\n clientId: string;\r\n clientSecret: string;\r\n secret: string;\r\n redirectUri: string;\r\n logoutRedirectUri?: string;\r\n deviceId?: string;\r\n scope?: string;\r\n audience?: string;\r\n jwks?: string;\r\n issuer?: string;\r\n}\r\n\r\ninterface UniDirAction {\r\n login: string;\r\n loginPath: string;\r\n logout: string;\r\n callback: string;\r\n me: string;\r\n}\r\nconst defaultActions: UniDirAction = {\r\n login: \"login\",\r\n loginPath: \"/api/auth/login\",\r\n logout: \"logout\",\r\n callback: \"callback\",\r\n me: \"me\",\r\n};\r\n\r\nexport function initUniDir(config: UniDirConfig, actions?: UniDirAction) {\r\n const uniDirActions = { ...defaultActions, ...actions };\r\n\r\n const getSession = async (req: Request | NextRequest) => {\r\n const cookieHeader = req.headers.get(\"cookie\") || \"\";\r\n const cookies = parse(cookieHeader);\r\n const sessionToken = cookies[\"unidir_session\"];\r\n if (!sessionToken) return null;\r\n return await decrypt(sessionToken, config.secret);\r\n };\r\n\r\n return {\r\n handleAuth: () => async (req: NextRequest) => {\r\n const action = req.nextUrl.pathname.split(\"/\").pop();\r\n // Capture the deviceId from the query param sent by the LoginButton\r\n const queryDeviceId = req.nextUrl.searchParams.get(\"device_id\");\r\n const effectiveDeviceId =\r\n queryDeviceId || config.deviceId || \"unknown-device\";\r\n //const deviceId = req.headers.get(\"x-device-id\");\r\n\r\n if (action === uniDirActions.login) {\r\n const returnTo = req.nextUrl.searchParams.get(\"returnTo\") || \"/\";\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n\r\n const url = new URL(`${config.domain}/authorize`);\r\n url.searchParams.set(\"client_id\", config.clientId);\r\n url.searchParams.set(\"response_type\", \"code\");\r\n url.searchParams.set(\"redirect_uri\", config.redirectUri);\r\n url.searchParams.set(\"scope\", \"openid profile email\");\r\n url.searchParams.set(\"code_challenge\", challenge);\r\n url.searchParams.set(\"code_challenge_method\", \"S256\");\r\n if (effectiveDeviceId) {\r\n url.searchParams.set(\"device_id\", effectiveDeviceId);\r\n }\r\n const response = NextResponse.redirect(url.toString());\r\n\r\n // Store verifier in a short-lived, secure cookie\r\n response.cookies.set(\"unidir_pkce_verifier\", verifier, {\r\n httpOnly: true,\r\n secure: true,\r\n sameSite: \"lax\",\r\n maxAge: 60 * 5, // 5 minutes\r\n });\r\n response.cookies.set(\"unidir_device_id\", effectiveDeviceId, {\r\n httpOnly: true,\r\n secure: true,\r\n maxAge: 60 * 10, // 10 minutes\r\n });\r\n response.cookies.set(\"unidir_return_to\", returnTo, {\r\n httpOnly: true,\r\n maxAge: 60 * 5,\r\n });\r\n return response;\r\n }\r\n // Profile Handler (for useUser hook)\r\n if (action === uniDirActions.me) {\r\n const session = await getSession(req);\r\n if (!session)\r\n return new NextResponse(JSON.stringify({ user: null }), {\r\n status: 401,\r\n });\r\n return NextResponse.json(session);\r\n }\r\n\r\n // Logout Handler\r\n if (action === uniDirActions.logout) {\r\n const response = NextResponse.redirect(new URL(\"/\", req.url));\r\n response.cookies.delete(\"unidir_session\");\r\n return response;\r\n }\r\n if (action === \"callback\") {\r\n const code = req.nextUrl.searchParams.get(\"code\");\r\n const verifier = req.cookies.get(\"unidir_pkce_verifier\")?.value;\r\n\r\n if (!code || !verifier) {\r\n return new NextResponse(\"Missing code or verifier\", { status: 400 });\r\n }\r\n\r\n const storedDeviceId = req.cookies.get(\"unidir_device_id\")?.value;\r\n const deviceIdToUse = storedDeviceId || effectiveDeviceId;\r\n\r\n const res = await fetch(`${config.domain}/token`, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n \"x-device-id\": deviceIdToUse, // Added to header\r\n },\r\n body: JSON.stringify({\r\n grant_type: \"authorization_code\",\r\n client_id: config.clientId,\r\n client_secret: config.clientSecret,\r\n code,\r\n code_verifier: verifier, // Pass the verifier back\r\n redirect_uri: config.redirectUri,\r\n device_id: deviceIdToUse,\r\n }),\r\n });\r\n\r\n const tokens = await res.json();\r\n if (tokens.error) return NextResponse.json(tokens, { status: 400 });\r\n\r\n const encryptedSession = await encrypt(tokens, config.secret);\r\n // const response = NextResponse.redirect(new URL(\"/\", req.url));\r\n const returnTo = req.cookies.get(\"unidir_return_to\")?.value || \"/\";\r\n const response = NextResponse.redirect(new URL(returnTo, req.url));\r\n\r\n // Clean up PKCE cookie and set session\r\n response.cookies.delete(\"unidir_pkce_verifier\");\r\n response.cookies.set(\"unidir_session\", encryptedSession, {\r\n httpOnly: true,\r\n secure: true,\r\n sameSite: \"lax\",\r\n maxAge: 60 * 60 * 24,\r\n });\r\n\r\n return response;\r\n }\r\n\r\n return new NextResponse(\"Not Found\", { status: 404 });\r\n },\r\n\r\n getSession,\r\n\r\n withPageAuthRequired: <P extends object>(\r\n Component: React.ComponentType<P>\r\n ) => {\r\n return async (props: P) => {\r\n // Import headers dynamically to avoid issues in non-server environments\r\n const { headers } = await import(\"next/headers\");\r\n const session = await getSession({ headers: await headers() } as any);\r\n\r\n if (!session) {\r\n redirect(uniDirActions.loginPath);\r\n }\r\n\r\n // Return the component as JSX\r\n return <Component {...props} user={session} />;\r\n };\r\n },\r\n\r\n withMiddlewareAuth: () => {\r\n return async (req: NextRequest) => {\r\n const sessionToken = req.cookies.get(\"unidir_session\")?.value;\r\n const session = sessionToken\r\n ? await decrypt(sessionToken, config.secret)\r\n : null;\r\n\r\n if (!session) {\r\n // Redirect to login but save the current URL to return back later\r\n const { pathname, search } = req.nextUrl;\r\n const url = new URL(uniDirActions.loginPath, req.url);\r\n url.searchParams.set(\"returnTo\", `${pathname}${search}`);\r\n return NextResponse.redirect(url);\r\n }\r\n\r\n return NextResponse.next();\r\n };\r\n },\r\n };\r\n}\r\n\r\nexport { UserProvider, useUser } from \"./client\";\r\n","import { EncryptJWT, jwtDecrypt } from \"jose\";\r\n\r\nconst getSecretKey = (secret: string) =>\r\n new TextEncoder().encode(secret.padEnd(32, \"0\").slice(0, 32));\r\n\r\nexport async function encrypt(payload: any, secret: string) {\r\n return new EncryptJWT(payload)\r\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\r\n .setIssuedAt()\r\n .setExpirationTime(\"24h\")\r\n .encrypt(getSecretKey(secret));\r\n}\r\n\r\nexport async function decrypt(token: string, secret: string) {\r\n try {\r\n const { payload } = await jwtDecrypt(token, getSecretKey(secret));\r\n return payload;\r\n } catch (e) {\r\n return null;\r\n }\r\n}\r\n","export function generateCodeVerifier(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return btoa(String.fromCharCode(...array))\r\n .replace(/\\+/g, \"-\")\r\n .replace(/\\//g, \"_\")\r\n .replace(/=/g, \"\");\r\n}\r\n\r\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(verifier);\r\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\r\n return btoa(String.fromCharCode(...new Uint8Array(digest)))\r\n .replace(/\\+/g, \"-\")\r\n .replace(/\\//g, \"_\")\r\n .replace(/=/g, \"\");\r\n}\r\n","\"use client\";\r\nimport React, { createContext, useContext, useState, useEffect } from \"react\";\r\nimport { UniDirConfig } from \".\";\r\nimport { verifyAccessToken } from \"./jwks\";\r\nimport { JWTPayload } from \"jose\";\r\n\r\nconst UserContext = createContext<{\r\n user: any;\r\n isLoading: boolean;\r\n config: UniDirConfig | null;\r\n}>({ user: null, isLoading: true, config: null });\r\n\r\nexport function UserProvider({\r\n children,\r\n config,\r\n}: {\r\n children: React.ReactNode;\r\n config: UniDirConfig;\r\n}) {\r\n const [user, setUser] = useState<JWTPayload | null>(null);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [token, setToken] = useState(null);\r\n const [accessToken, setAccessToken] = useState(null);\r\n const [refreshToken, setRefreshToken] = useState(null);\r\n const [idToken, setIdToken] = useState(null);\r\n const [client, setClient] = useState(null);\r\n const [expiresIn, setExpiresIn] = useState(null);\r\n\r\n // useEffect(() => {\r\n // fetch(\"/api/auth/me\")\r\n // .then((res) => res.json())\r\n // .then((data) => {\r\n // if (data) {\r\n // setUser(data.client);\r\n // setToken(data);\r\n // setAccessToken(data.access_token);\r\n // setRefreshToken(data.refresh_token);\r\n // const idTokenAll = await jwtVerify(\r\n // data.id_token,\r\n // config.jwks || \"https://oauth.biocloud.pro/jwks.json\"\r\n // );\r\n // setIdToken(data.id_token);\r\n // setClient(data.client);\r\n // setExpiresIn(data.expres_in);\r\n // }\r\n // })\r\n // .catch(() => setUser(null));\r\n // }, []);\r\n useEffect(() => {\r\n async function loadUser() {\r\n try {\r\n const res = await fetch(\"/api/auth/me\");\r\n const data = await res.json();\r\n //setUser(data.user);\r\n const { companyId, domainId, email, email_verified, name } =\r\n await verifyAccessToken(\r\n data.id_token,\r\n config.jwks || \"https://oauth.igoodworks.com/jwks.json\",\r\n {\r\n issuer: config.issuer || \"http://oauth.unidir.igoodworks.com/\",\r\n audience: config.clientId,\r\n }\r\n );\r\n setUser({ companyId, domainId, email, email_verified, name });\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }\r\n loadUser();\r\n }, []);\r\n return (\r\n <UserContext.Provider\r\n value={{\r\n user,\r\n isLoading,\r\n config,\r\n // token,\r\n // expiresIn,\r\n // accessToken,\r\n // refreshToken,\r\n // client,\r\n // idToken,\r\n }}\r\n >\r\n {children}\r\n </UserContext.Provider>\r\n );\r\n}\r\n\r\nexport function getDeviceId(): string {\r\n if (typeof window === \"undefined\") return \"server-default\";\r\n\r\n let id = localStorage.getItem(\"unidir_device_id\");\r\n if (!id) {\r\n id = crypto.randomUUID();\r\n localStorage.setItem(\"unidir_device_id\", id);\r\n }\r\n return id;\r\n}\r\n\r\nexport const useUser = () => useContext(UserContext);\r\n","import { jwtVerify, createRemoteJWKSet } from \"jose\";\r\n\r\n// Replace with your IdP issuer and audience\r\nconst ISSUER = \"https://YOUR_ISSUER/\";\r\nconst AUDIENCE = \"YOUR_CLIENT_ID\";\r\n\r\nexport async function verifyAccessToken(\r\n token: string,\r\n jwksUrl: string,\r\n options: Record<string, any> = {}\r\n) {\r\n const JWKS = createRemoteJWKSet(new URL(jwksUrl));\r\n const { payload } = await jwtVerify(token, JWKS, {\r\n issuer: options.issuer,\r\n audience: options.audience,\r\n });\r\n // Optionally apply custom claims checks here (e.g., roles, scopes)\r\n return payload;\r\n}\r\n"],"mappings":"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","names":["index_exports","__export","UserProvider","initUniDir","useUser","__toCommonJS","import_cookie","import_jose","getSecretKey","secret","encrypt","payload","decrypt","token","import_server","import_navigation","generateCodeVerifier","array","generateCodeChallenge","verifier","data","digest","import_react","import_jose","verifyAccessToken","token","jwksUrl","options","JWKS","payload","import_jsx_runtime","UserContext","UserProvider","children","config","user","setUser","isLoading","setIsLoading","token","setToken","accessToken","setAccessToken","refreshToken","setRefreshToken","idToken","setIdToken","client","setClient","expiresIn","setExpiresIn","loadUser","data","companyId","domainId","email","email_verified","name","verifyAccessToken","useUser","UserContext","import_jsx_runtime","defaultActions","initUniDir","config","actions","uniDirActions","getSession","req","cookieHeader","sessionToken","decrypt","action","effectiveDeviceId","returnTo","verifier","generateCodeVerifier","challenge","generateCodeChallenge","url","response","session","code","deviceIdToUse","tokens","encryptedSession","encrypt","Component","props","headers","pathname","search"]}
@@ -0,0 +1,43 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as jose from 'jose';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import React$1 from 'react';
5
+
6
+ declare function UserProvider({ children, config, }: {
7
+ children: React$1.ReactNode;
8
+ config: UniDirConfig;
9
+ }): react_jsx_runtime.JSX.Element;
10
+ declare const useUser: () => {
11
+ user: any;
12
+ isLoading: boolean;
13
+ config: UniDirConfig | null;
14
+ };
15
+
16
+ interface UniDirConfig {
17
+ domain: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ secret: string;
21
+ redirectUri: string;
22
+ logoutRedirectUri?: string;
23
+ deviceId?: string;
24
+ scope?: string;
25
+ audience?: string;
26
+ jwks?: string;
27
+ issuer?: string;
28
+ }
29
+ interface UniDirAction {
30
+ login: string;
31
+ loginPath: string;
32
+ logout: string;
33
+ callback: string;
34
+ me: string;
35
+ }
36
+ declare function initUniDir(config: UniDirConfig, actions?: UniDirAction): {
37
+ handleAuth: () => (req: NextRequest) => Promise<NextResponse<any>>;
38
+ getSession: (req: Request | NextRequest) => Promise<jose.JWTPayload | null>;
39
+ withPageAuthRequired: <P extends object>(Component: React.ComponentType<P>) => (props: P) => Promise<react_jsx_runtime.JSX.Element>;
40
+ withMiddlewareAuth: () => (req: NextRequest) => Promise<NextResponse<unknown>>;
41
+ };
42
+
43
+ export { type UniDirConfig, UserProvider, initUniDir, useUser };
@@ -0,0 +1,43 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as jose from 'jose';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import React$1 from 'react';
5
+
6
+ declare function UserProvider({ children, config, }: {
7
+ children: React$1.ReactNode;
8
+ config: UniDirConfig;
9
+ }): react_jsx_runtime.JSX.Element;
10
+ declare const useUser: () => {
11
+ user: any;
12
+ isLoading: boolean;
13
+ config: UniDirConfig | null;
14
+ };
15
+
16
+ interface UniDirConfig {
17
+ domain: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ secret: string;
21
+ redirectUri: string;
22
+ logoutRedirectUri?: string;
23
+ deviceId?: string;
24
+ scope?: string;
25
+ audience?: string;
26
+ jwks?: string;
27
+ issuer?: string;
28
+ }
29
+ interface UniDirAction {
30
+ login: string;
31
+ loginPath: string;
32
+ logout: string;
33
+ callback: string;
34
+ me: string;
35
+ }
36
+ declare function initUniDir(config: UniDirConfig, actions?: UniDirAction): {
37
+ handleAuth: () => (req: NextRequest) => Promise<NextResponse<any>>;
38
+ getSession: (req: Request | NextRequest) => Promise<jose.JWTPayload | null>;
39
+ withPageAuthRequired: <P extends object>(Component: React.ComponentType<P>) => (props: P) => Promise<react_jsx_runtime.JSX.Element>;
40
+ withMiddlewareAuth: () => (req: NextRequest) => Promise<NextResponse<unknown>>;
41
+ };
42
+
43
+ export { type UniDirConfig, UserProvider, initUniDir, useUser };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{parse as K}from"cookie";import{EncryptJWT as N,jwtDecrypt as b}from"jose";var w=e=>new TextEncoder().encode(e.padEnd(32,"0").slice(0,32));async function U(e,n){return new N(e).setProtectedHeader({alg:"dir",enc:"A256GCM"}).setIssuedAt().setExpirationTime("24h").encrypt(w(n))}async function y(e,n){try{let{payload:r}=await b(e,w(n));return r}catch{return null}}import{NextResponse as a}from"next/server";import{redirect as M}from"next/navigation";function v(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function x(e){let r=new TextEncoder().encode(e),d=await crypto.subtle.digest("SHA-256",r);return btoa(String.fromCharCode(...new Uint8Array(d))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}import{createContext as O,useContext as J,useState as l,useEffect as W}from"react";import{jwtVerify as j,createRemoteJWKSet as L}from"jose";async function I(e,n,r={}){let d=L(new URL(n)),{payload:t}=await j(e,d,{issuer:r.issuer,audience:r.audience});return t}import{jsx as H}from"react/jsx-runtime";var P=O({user:null,isLoading:!0,config:null});function V({children:e,config:n}){let[r,d]=l(null),[t,i]=l(!0),[p,s]=l(null),[o,u]=l(null),[h,c]=l(null),[g,m]=l(null),[_,k]=l(null),[f,F]=l(null);return W(()=>{async function T(){try{let C=await(await fetch("/api/auth/me")).json(),{companyId:R,domainId:S,email:A,email_verified:D,name:E}=await I(C.id_token,n.jwks||"https://oauth.igoodworks.com/jwks.json",{issuer:n.issuer||"http://oauth.unidir.igoodworks.com/",audience:n.clientId});d({companyId:R,domainId:S,email:A,email_verified:D,name:E})}finally{i(!1)}}T()},[]),H(P.Provider,{value:{user:r,isLoading:t,config:n},children:e})}var $=()=>J(P);import{jsx as Y}from"react/jsx-runtime";var z={login:"login",loginPath:"/api/auth/login",logout:"logout",callback:"callback",me:"me"};function le(e,n){let r={...z,...n},d=async t=>{let i=t.headers.get("cookie")||"",s=K(i).unidir_session;return s?await y(s,e.secret):null};return{handleAuth:()=>async t=>{let i=t.nextUrl.pathname.split("/").pop(),s=t.nextUrl.searchParams.get("device_id")||e.deviceId||"unknown-device";if(i===r.login){let o=t.nextUrl.searchParams.get("returnTo")||"/",u=v(),h=await x(u),c=new URL(`${e.domain}/authorize`);c.searchParams.set("client_id",e.clientId),c.searchParams.set("response_type","code"),c.searchParams.set("redirect_uri",e.redirectUri),c.searchParams.set("scope","openid profile email"),c.searchParams.set("code_challenge",h),c.searchParams.set("code_challenge_method","S256"),s&&c.searchParams.set("device_id",s);let g=a.redirect(c.toString());return g.cookies.set("unidir_pkce_verifier",u,{httpOnly:!0,secure:!0,sameSite:"lax",maxAge:300}),g.cookies.set("unidir_device_id",s,{httpOnly:!0,secure:!0,maxAge:600}),g.cookies.set("unidir_return_to",o,{httpOnly:!0,maxAge:300}),g}if(i===r.me){let o=await d(t);return o?a.json(o):new a(JSON.stringify({user:null}),{status:401})}if(i===r.logout){let o=a.redirect(new URL("/",t.url));return o.cookies.delete("unidir_session"),o}if(i==="callback"){let o=t.nextUrl.searchParams.get("code"),u=t.cookies.get("unidir_pkce_verifier")?.value;if(!o||!u)return new a("Missing code or verifier",{status:400});let c=t.cookies.get("unidir_device_id")?.value||s,m=await(await fetch(`${e.domain}/token`,{method:"POST",headers:{"Content-Type":"application/json","x-device-id":c},body:JSON.stringify({grant_type:"authorization_code",client_id:e.clientId,client_secret:e.clientSecret,code:o,code_verifier:u,redirect_uri:e.redirectUri,device_id:c})})).json();if(m.error)return a.json(m,{status:400});let _=await U(m,e.secret),k=t.cookies.get("unidir_return_to")?.value||"/",f=a.redirect(new URL(k,t.url));return f.cookies.delete("unidir_pkce_verifier"),f.cookies.set("unidir_session",_,{httpOnly:!0,secure:!0,sameSite:"lax",maxAge:3600*24}),f}return new a("Not Found",{status:404})},getSession:d,withPageAuthRequired:t=>async i=>{let{headers:p}=await import("next/headers"),s=await d({headers:await p()});return s||M(r.loginPath),Y(t,{...i,user:s})},withMiddlewareAuth:()=>async t=>{let i=t.cookies.get("unidir_session")?.value;if(!(i?await y(i,e.secret):null)){let{pathname:s,search:o}=t.nextUrl,u=new URL(r.loginPath,t.url);return u.searchParams.set("returnTo",`${s}${o}`),a.redirect(u)}return a.next()}}}export{V as UserProvider,le as initUniDir,$ as useUser};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx","../src/session.ts","../src/pkce.ts","../src/client.tsx","../src/jwks.ts"],"sourcesContent":["import { parse } from \"cookie\";\r\nimport { encrypt, decrypt } from \"./session\";\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport { redirect } from \"next/navigation\";\r\nimport { generateCodeVerifier, generateCodeChallenge } from \"./pkce\";\r\n\r\nexport interface UniDirConfig {\r\n domain: string;\r\n clientId: string;\r\n clientSecret: string;\r\n secret: string;\r\n redirectUri: string;\r\n logoutRedirectUri?: string;\r\n deviceId?: string;\r\n scope?: string;\r\n audience?: string;\r\n jwks?: string;\r\n issuer?: string;\r\n}\r\n\r\ninterface UniDirAction {\r\n login: string;\r\n loginPath: string;\r\n logout: string;\r\n callback: string;\r\n me: string;\r\n}\r\nconst defaultActions: UniDirAction = {\r\n login: \"login\",\r\n loginPath: \"/api/auth/login\",\r\n logout: \"logout\",\r\n callback: \"callback\",\r\n me: \"me\",\r\n};\r\n\r\nexport function initUniDir(config: UniDirConfig, actions?: UniDirAction) {\r\n const uniDirActions = { ...defaultActions, ...actions };\r\n\r\n const getSession = async (req: Request | NextRequest) => {\r\n const cookieHeader = req.headers.get(\"cookie\") || \"\";\r\n const cookies = parse(cookieHeader);\r\n const sessionToken = cookies[\"unidir_session\"];\r\n if (!sessionToken) return null;\r\n return await decrypt(sessionToken, config.secret);\r\n };\r\n\r\n return {\r\n handleAuth: () => async (req: NextRequest) => {\r\n const action = req.nextUrl.pathname.split(\"/\").pop();\r\n // Capture the deviceId from the query param sent by the LoginButton\r\n const queryDeviceId = req.nextUrl.searchParams.get(\"device_id\");\r\n const effectiveDeviceId =\r\n queryDeviceId || config.deviceId || \"unknown-device\";\r\n //const deviceId = req.headers.get(\"x-device-id\");\r\n\r\n if (action === uniDirActions.login) {\r\n const returnTo = req.nextUrl.searchParams.get(\"returnTo\") || \"/\";\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n\r\n const url = new URL(`${config.domain}/authorize`);\r\n url.searchParams.set(\"client_id\", config.clientId);\r\n url.searchParams.set(\"response_type\", \"code\");\r\n url.searchParams.set(\"redirect_uri\", config.redirectUri);\r\n url.searchParams.set(\"scope\", \"openid profile email\");\r\n url.searchParams.set(\"code_challenge\", challenge);\r\n url.searchParams.set(\"code_challenge_method\", \"S256\");\r\n if (effectiveDeviceId) {\r\n url.searchParams.set(\"device_id\", effectiveDeviceId);\r\n }\r\n const response = NextResponse.redirect(url.toString());\r\n\r\n // Store verifier in a short-lived, secure cookie\r\n response.cookies.set(\"unidir_pkce_verifier\", verifier, {\r\n httpOnly: true,\r\n secure: true,\r\n sameSite: \"lax\",\r\n maxAge: 60 * 5, // 5 minutes\r\n });\r\n response.cookies.set(\"unidir_device_id\", effectiveDeviceId, {\r\n httpOnly: true,\r\n secure: true,\r\n maxAge: 60 * 10, // 10 minutes\r\n });\r\n response.cookies.set(\"unidir_return_to\", returnTo, {\r\n httpOnly: true,\r\n maxAge: 60 * 5,\r\n });\r\n return response;\r\n }\r\n // Profile Handler (for useUser hook)\r\n if (action === uniDirActions.me) {\r\n const session = await getSession(req);\r\n if (!session)\r\n return new NextResponse(JSON.stringify({ user: null }), {\r\n status: 401,\r\n });\r\n return NextResponse.json(session);\r\n }\r\n\r\n // Logout Handler\r\n if (action === uniDirActions.logout) {\r\n const response = NextResponse.redirect(new URL(\"/\", req.url));\r\n response.cookies.delete(\"unidir_session\");\r\n return response;\r\n }\r\n if (action === \"callback\") {\r\n const code = req.nextUrl.searchParams.get(\"code\");\r\n const verifier = req.cookies.get(\"unidir_pkce_verifier\")?.value;\r\n\r\n if (!code || !verifier) {\r\n return new NextResponse(\"Missing code or verifier\", { status: 400 });\r\n }\r\n\r\n const storedDeviceId = req.cookies.get(\"unidir_device_id\")?.value;\r\n const deviceIdToUse = storedDeviceId || effectiveDeviceId;\r\n\r\n const res = await fetch(`${config.domain}/token`, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n \"x-device-id\": deviceIdToUse, // Added to header\r\n },\r\n body: JSON.stringify({\r\n grant_type: \"authorization_code\",\r\n client_id: config.clientId,\r\n client_secret: config.clientSecret,\r\n code,\r\n code_verifier: verifier, // Pass the verifier back\r\n redirect_uri: config.redirectUri,\r\n device_id: deviceIdToUse,\r\n }),\r\n });\r\n\r\n const tokens = await res.json();\r\n if (tokens.error) return NextResponse.json(tokens, { status: 400 });\r\n\r\n const encryptedSession = await encrypt(tokens, config.secret);\r\n // const response = NextResponse.redirect(new URL(\"/\", req.url));\r\n const returnTo = req.cookies.get(\"unidir_return_to\")?.value || \"/\";\r\n const response = NextResponse.redirect(new URL(returnTo, req.url));\r\n\r\n // Clean up PKCE cookie and set session\r\n response.cookies.delete(\"unidir_pkce_verifier\");\r\n response.cookies.set(\"unidir_session\", encryptedSession, {\r\n httpOnly: true,\r\n secure: true,\r\n sameSite: \"lax\",\r\n maxAge: 60 * 60 * 24,\r\n });\r\n\r\n return response;\r\n }\r\n\r\n return new NextResponse(\"Not Found\", { status: 404 });\r\n },\r\n\r\n getSession,\r\n\r\n withPageAuthRequired: <P extends object>(\r\n Component: React.ComponentType<P>\r\n ) => {\r\n return async (props: P) => {\r\n // Import headers dynamically to avoid issues in non-server environments\r\n const { headers } = await import(\"next/headers\");\r\n const session = await getSession({ headers: await headers() } as any);\r\n\r\n if (!session) {\r\n redirect(uniDirActions.loginPath);\r\n }\r\n\r\n // Return the component as JSX\r\n return <Component {...props} user={session} />;\r\n };\r\n },\r\n\r\n withMiddlewareAuth: () => {\r\n return async (req: NextRequest) => {\r\n const sessionToken = req.cookies.get(\"unidir_session\")?.value;\r\n const session = sessionToken\r\n ? await decrypt(sessionToken, config.secret)\r\n : null;\r\n\r\n if (!session) {\r\n // Redirect to login but save the current URL to return back later\r\n const { pathname, search } = req.nextUrl;\r\n const url = new URL(uniDirActions.loginPath, req.url);\r\n url.searchParams.set(\"returnTo\", `${pathname}${search}`);\r\n return NextResponse.redirect(url);\r\n }\r\n\r\n return NextResponse.next();\r\n };\r\n },\r\n };\r\n}\r\n\r\nexport { UserProvider, useUser } from \"./client\";\r\n","import { EncryptJWT, jwtDecrypt } from \"jose\";\r\n\r\nconst getSecretKey = (secret: string) =>\r\n new TextEncoder().encode(secret.padEnd(32, \"0\").slice(0, 32));\r\n\r\nexport async function encrypt(payload: any, secret: string) {\r\n return new EncryptJWT(payload)\r\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\r\n .setIssuedAt()\r\n .setExpirationTime(\"24h\")\r\n .encrypt(getSecretKey(secret));\r\n}\r\n\r\nexport async function decrypt(token: string, secret: string) {\r\n try {\r\n const { payload } = await jwtDecrypt(token, getSecretKey(secret));\r\n return payload;\r\n } catch (e) {\r\n return null;\r\n }\r\n}\r\n","export function generateCodeVerifier(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return btoa(String.fromCharCode(...array))\r\n .replace(/\\+/g, \"-\")\r\n .replace(/\\//g, \"_\")\r\n .replace(/=/g, \"\");\r\n}\r\n\r\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(verifier);\r\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\r\n return btoa(String.fromCharCode(...new Uint8Array(digest)))\r\n .replace(/\\+/g, \"-\")\r\n .replace(/\\//g, \"_\")\r\n .replace(/=/g, \"\");\r\n}\r\n","\"use client\";\r\nimport React, { createContext, useContext, useState, useEffect } from \"react\";\r\nimport { UniDirConfig } from \".\";\r\nimport { verifyAccessToken } from \"./jwks\";\r\nimport { JWTPayload } from \"jose\";\r\n\r\nconst UserContext = createContext<{\r\n user: any;\r\n isLoading: boolean;\r\n config: UniDirConfig | null;\r\n}>({ user: null, isLoading: true, config: null });\r\n\r\nexport function UserProvider({\r\n children,\r\n config,\r\n}: {\r\n children: React.ReactNode;\r\n config: UniDirConfig;\r\n}) {\r\n const [user, setUser] = useState<JWTPayload | null>(null);\r\n const [isLoading, setIsLoading] = useState(true);\r\n const [token, setToken] = useState(null);\r\n const [accessToken, setAccessToken] = useState(null);\r\n const [refreshToken, setRefreshToken] = useState(null);\r\n const [idToken, setIdToken] = useState(null);\r\n const [client, setClient] = useState(null);\r\n const [expiresIn, setExpiresIn] = useState(null);\r\n\r\n // useEffect(() => {\r\n // fetch(\"/api/auth/me\")\r\n // .then((res) => res.json())\r\n // .then((data) => {\r\n // if (data) {\r\n // setUser(data.client);\r\n // setToken(data);\r\n // setAccessToken(data.access_token);\r\n // setRefreshToken(data.refresh_token);\r\n // const idTokenAll = await jwtVerify(\r\n // data.id_token,\r\n // config.jwks || \"https://oauth.biocloud.pro/jwks.json\"\r\n // );\r\n // setIdToken(data.id_token);\r\n // setClient(data.client);\r\n // setExpiresIn(data.expres_in);\r\n // }\r\n // })\r\n // .catch(() => setUser(null));\r\n // }, []);\r\n useEffect(() => {\r\n async function loadUser() {\r\n try {\r\n const res = await fetch(\"/api/auth/me\");\r\n const data = await res.json();\r\n //setUser(data.user);\r\n const { companyId, domainId, email, email_verified, name } =\r\n await verifyAccessToken(\r\n data.id_token,\r\n config.jwks || \"https://oauth.igoodworks.com/jwks.json\",\r\n {\r\n issuer: config.issuer || \"http://oauth.unidir.igoodworks.com/\",\r\n audience: config.clientId,\r\n }\r\n );\r\n setUser({ companyId, domainId, email, email_verified, name });\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }\r\n loadUser();\r\n }, []);\r\n return (\r\n <UserContext.Provider\r\n value={{\r\n user,\r\n isLoading,\r\n config,\r\n // token,\r\n // expiresIn,\r\n // accessToken,\r\n // refreshToken,\r\n // client,\r\n // idToken,\r\n }}\r\n >\r\n {children}\r\n </UserContext.Provider>\r\n );\r\n}\r\n\r\nexport function getDeviceId(): string {\r\n if (typeof window === \"undefined\") return \"server-default\";\r\n\r\n let id = localStorage.getItem(\"unidir_device_id\");\r\n if (!id) {\r\n id = crypto.randomUUID();\r\n localStorage.setItem(\"unidir_device_id\", id);\r\n }\r\n return id;\r\n}\r\n\r\nexport const useUser = () => useContext(UserContext);\r\n","import { jwtVerify, createRemoteJWKSet } from \"jose\";\r\n\r\n// Replace with your IdP issuer and audience\r\nconst ISSUER = \"https://YOUR_ISSUER/\";\r\nconst AUDIENCE = \"YOUR_CLIENT_ID\";\r\n\r\nexport async function verifyAccessToken(\r\n token: string,\r\n jwksUrl: string,\r\n options: Record<string, any> = {}\r\n) {\r\n const JWKS = createRemoteJWKSet(new URL(jwksUrl));\r\n const { payload } = await jwtVerify(token, JWKS, {\r\n issuer: options.issuer,\r\n audience: options.audience,\r\n });\r\n // Optionally apply custom claims checks here (e.g., roles, scopes)\r\n return payload;\r\n}\r\n"],"mappings":"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","names":["parse","EncryptJWT","jwtDecrypt","getSecretKey","secret","encrypt","payload","decrypt","token","NextResponse","redirect","generateCodeVerifier","array","generateCodeChallenge","verifier","data","digest","createContext","useContext","useState","useEffect","jwtVerify","createRemoteJWKSet","verifyAccessToken","token","jwksUrl","options","JWKS","createRemoteJWKSet","payload","jwtVerify","jsx","UserContext","createContext","UserProvider","children","config","user","setUser","useState","isLoading","setIsLoading","token","setToken","accessToken","setAccessToken","refreshToken","setRefreshToken","idToken","setIdToken","client","setClient","expiresIn","setExpiresIn","useEffect","loadUser","data","companyId","domainId","email","email_verified","name","verifyAccessToken","useUser","useContext","UserContext","jsx","defaultActions","initUniDir","config","actions","uniDirActions","getSession","req","cookieHeader","sessionToken","parse","decrypt","action","effectiveDeviceId","returnTo","verifier","generateCodeVerifier","challenge","generateCodeChallenge","url","response","NextResponse","session","code","deviceIdToUse","tokens","encryptedSession","encrypt","Component","props","headers","redirect","pathname","search"]}
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@unidir/unidir-nextjs",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
8
11
  "publishConfig": {
9
12
  "access": "public"
10
13
  },
package/src/client.tsx DELETED
@@ -1,101 +0,0 @@
1
- "use client";
2
- import React, { createContext, useContext, useState, useEffect } from "react";
3
- import { UniDirConfig } from ".";
4
- import { verifyAccessToken } from "./jwks";
5
- import { JWTPayload } from "jose";
6
-
7
- const UserContext = createContext<{
8
- user: any;
9
- isLoading: boolean;
10
- config: UniDirConfig | null;
11
- }>({ user: null, isLoading: true, config: null });
12
-
13
- export function UserProvider({
14
- children,
15
- config,
16
- }: {
17
- children: React.ReactNode;
18
- config: UniDirConfig;
19
- }) {
20
- const [user, setUser] = useState<JWTPayload | null>(null);
21
- const [isLoading, setIsLoading] = useState(true);
22
- const [token, setToken] = useState(null);
23
- const [accessToken, setAccessToken] = useState(null);
24
- const [refreshToken, setRefreshToken] = useState(null);
25
- const [idToken, setIdToken] = useState(null);
26
- const [client, setClient] = useState(null);
27
- const [expiresIn, setExpiresIn] = useState(null);
28
-
29
- // useEffect(() => {
30
- // fetch("/api/auth/me")
31
- // .then((res) => res.json())
32
- // .then((data) => {
33
- // if (data) {
34
- // setUser(data.client);
35
- // setToken(data);
36
- // setAccessToken(data.access_token);
37
- // setRefreshToken(data.refresh_token);
38
- // const idTokenAll = await jwtVerify(
39
- // data.id_token,
40
- // config.jwks || "https://oauth.biocloud.pro/jwks.json"
41
- // );
42
- // setIdToken(data.id_token);
43
- // setClient(data.client);
44
- // setExpiresIn(data.expres_in);
45
- // }
46
- // })
47
- // .catch(() => setUser(null));
48
- // }, []);
49
- useEffect(() => {
50
- async function loadUser() {
51
- try {
52
- const res = await fetch("/api/auth/me");
53
- const data = await res.json();
54
- //setUser(data.user);
55
- const { companyId, domainId, email, email_verified, name } =
56
- await verifyAccessToken(
57
- data.id_token,
58
- config.jwks || "https://oauth.igoodworks.com/jwks.json",
59
- {
60
- issuer: config.issuer || "http://oauth.unidir.igoodworks.com/",
61
- audience: config.clientId,
62
- }
63
- );
64
- setUser({ companyId, domainId, email, email_verified, name });
65
- } finally {
66
- setIsLoading(false);
67
- }
68
- }
69
- loadUser();
70
- }, []);
71
- return (
72
- <UserContext.Provider
73
- value={{
74
- user,
75
- isLoading,
76
- config,
77
- // token,
78
- // expiresIn,
79
- // accessToken,
80
- // refreshToken,
81
- // client,
82
- // idToken,
83
- }}
84
- >
85
- {children}
86
- </UserContext.Provider>
87
- );
88
- }
89
-
90
- export function getDeviceId(): string {
91
- if (typeof window === "undefined") return "server-default";
92
-
93
- let id = localStorage.getItem("unidir_device_id");
94
- if (!id) {
95
- id = crypto.randomUUID();
96
- localStorage.setItem("unidir_device_id", id);
97
- }
98
- return id;
99
- }
100
-
101
- export const useUser = () => useContext(UserContext);
package/src/index.tsx DELETED
@@ -1,198 +0,0 @@
1
- import { parse } from "cookie";
2
- import { encrypt, decrypt } from "./session";
3
- import { NextRequest, NextResponse } from "next/server";
4
- import { redirect } from "next/navigation";
5
- import { generateCodeVerifier, generateCodeChallenge } from "./pkce";
6
-
7
- export interface UniDirConfig {
8
- domain: string;
9
- clientId: string;
10
- clientSecret: string;
11
- secret: string;
12
- redirectUri: string;
13
- logoutRedirectUri?: string;
14
- deviceId?: string;
15
- scope?: string;
16
- audience?: string;
17
- jwks?: string;
18
- issuer?: string;
19
- }
20
-
21
- interface UniDirAction {
22
- login: string;
23
- loginPath: string;
24
- logout: string;
25
- callback: string;
26
- me: string;
27
- }
28
- const defaultActions: UniDirAction = {
29
- login: "login",
30
- loginPath: "/api/auth/login",
31
- logout: "logout",
32
- callback: "callback",
33
- me: "me",
34
- };
35
-
36
- export function initUniDir(config: UniDirConfig, actions?: UniDirAction) {
37
- const uniDirActions = { ...defaultActions, ...actions };
38
-
39
- const getSession = async (req: Request | NextRequest) => {
40
- const cookieHeader = req.headers.get("cookie") || "";
41
- const cookies = parse(cookieHeader);
42
- const sessionToken = cookies["unidir_session"];
43
- if (!sessionToken) return null;
44
- return await decrypt(sessionToken, config.secret);
45
- };
46
-
47
- return {
48
- handleAuth: () => async (req: NextRequest) => {
49
- const action = req.nextUrl.pathname.split("/").pop();
50
- // Capture the deviceId from the query param sent by the LoginButton
51
- const queryDeviceId = req.nextUrl.searchParams.get("device_id");
52
- const effectiveDeviceId =
53
- queryDeviceId || config.deviceId || "unknown-device";
54
- //const deviceId = req.headers.get("x-device-id");
55
-
56
- if (action === uniDirActions.login) {
57
- const returnTo = req.nextUrl.searchParams.get("returnTo") || "/";
58
- const verifier = generateCodeVerifier();
59
- const challenge = await generateCodeChallenge(verifier);
60
-
61
- const url = new URL(`${config.domain}/authorize`);
62
- url.searchParams.set("client_id", config.clientId);
63
- url.searchParams.set("response_type", "code");
64
- url.searchParams.set("redirect_uri", config.redirectUri);
65
- url.searchParams.set("scope", "openid profile email");
66
- url.searchParams.set("code_challenge", challenge);
67
- url.searchParams.set("code_challenge_method", "S256");
68
- if (effectiveDeviceId) {
69
- url.searchParams.set("device_id", effectiveDeviceId);
70
- }
71
- const response = NextResponse.redirect(url.toString());
72
-
73
- // Store verifier in a short-lived, secure cookie
74
- response.cookies.set("unidir_pkce_verifier", verifier, {
75
- httpOnly: true,
76
- secure: true,
77
- sameSite: "lax",
78
- maxAge: 60 * 5, // 5 minutes
79
- });
80
- response.cookies.set("unidir_device_id", effectiveDeviceId, {
81
- httpOnly: true,
82
- secure: true,
83
- maxAge: 60 * 10, // 10 minutes
84
- });
85
- response.cookies.set("unidir_return_to", returnTo, {
86
- httpOnly: true,
87
- maxAge: 60 * 5,
88
- });
89
- return response;
90
- }
91
- // Profile Handler (for useUser hook)
92
- if (action === uniDirActions.me) {
93
- const session = await getSession(req);
94
- if (!session)
95
- return new NextResponse(JSON.stringify({ user: null }), {
96
- status: 401,
97
- });
98
- return NextResponse.json(session);
99
- }
100
-
101
- // Logout Handler
102
- if (action === uniDirActions.logout) {
103
- const response = NextResponse.redirect(new URL("/", req.url));
104
- response.cookies.delete("unidir_session");
105
- return response;
106
- }
107
- if (action === "callback") {
108
- const code = req.nextUrl.searchParams.get("code");
109
- const verifier = req.cookies.get("unidir_pkce_verifier")?.value;
110
-
111
- if (!code || !verifier) {
112
- return new NextResponse("Missing code or verifier", { status: 400 });
113
- }
114
-
115
- const storedDeviceId = req.cookies.get("unidir_device_id")?.value;
116
- const deviceIdToUse = storedDeviceId || effectiveDeviceId;
117
-
118
- const res = await fetch(`${config.domain}/token`, {
119
- method: "POST",
120
- headers: {
121
- "Content-Type": "application/json",
122
- "x-device-id": deviceIdToUse, // Added to header
123
- },
124
- body: JSON.stringify({
125
- grant_type: "authorization_code",
126
- client_id: config.clientId,
127
- client_secret: config.clientSecret,
128
- code,
129
- code_verifier: verifier, // Pass the verifier back
130
- redirect_uri: config.redirectUri,
131
- device_id: deviceIdToUse,
132
- }),
133
- });
134
-
135
- const tokens = await res.json();
136
- if (tokens.error) return NextResponse.json(tokens, { status: 400 });
137
-
138
- const encryptedSession = await encrypt(tokens, config.secret);
139
- // const response = NextResponse.redirect(new URL("/", req.url));
140
- const returnTo = req.cookies.get("unidir_return_to")?.value || "/";
141
- const response = NextResponse.redirect(new URL(returnTo, req.url));
142
-
143
- // Clean up PKCE cookie and set session
144
- response.cookies.delete("unidir_pkce_verifier");
145
- response.cookies.set("unidir_session", encryptedSession, {
146
- httpOnly: true,
147
- secure: true,
148
- sameSite: "lax",
149
- maxAge: 60 * 60 * 24,
150
- });
151
-
152
- return response;
153
- }
154
-
155
- return new NextResponse("Not Found", { status: 404 });
156
- },
157
-
158
- getSession,
159
-
160
- withPageAuthRequired: <P extends object>(
161
- Component: React.ComponentType<P>
162
- ) => {
163
- return async (props: P) => {
164
- // Import headers dynamically to avoid issues in non-server environments
165
- const { headers } = await import("next/headers");
166
- const session = await getSession({ headers: await headers() } as any);
167
-
168
- if (!session) {
169
- redirect(uniDirActions.loginPath);
170
- }
171
-
172
- // Return the component as JSX
173
- return <Component {...props} user={session} />;
174
- };
175
- },
176
-
177
- withMiddlewareAuth: () => {
178
- return async (req: NextRequest) => {
179
- const sessionToken = req.cookies.get("unidir_session")?.value;
180
- const session = sessionToken
181
- ? await decrypt(sessionToken, config.secret)
182
- : null;
183
-
184
- if (!session) {
185
- // Redirect to login but save the current URL to return back later
186
- const { pathname, search } = req.nextUrl;
187
- const url = new URL(uniDirActions.loginPath, req.url);
188
- url.searchParams.set("returnTo", `${pathname}${search}`);
189
- return NextResponse.redirect(url);
190
- }
191
-
192
- return NextResponse.next();
193
- };
194
- },
195
- };
196
- }
197
-
198
- export { UserProvider, useUser } from "./client";
package/src/jwks.ts DELETED
@@ -1,19 +0,0 @@
1
- import { jwtVerify, createRemoteJWKSet } from "jose";
2
-
3
- // Replace with your IdP issuer and audience
4
- const ISSUER = "https://YOUR_ISSUER/";
5
- const AUDIENCE = "YOUR_CLIENT_ID";
6
-
7
- export async function verifyAccessToken(
8
- token: string,
9
- jwksUrl: string,
10
- options: Record<string, any> = {}
11
- ) {
12
- const JWKS = createRemoteJWKSet(new URL(jwksUrl));
13
- const { payload } = await jwtVerify(token, JWKS, {
14
- issuer: options.issuer,
15
- audience: options.audience,
16
- });
17
- // Optionally apply custom claims checks here (e.g., roles, scopes)
18
- return payload;
19
- }
package/src/pkce.ts DELETED
@@ -1,18 +0,0 @@
1
- export function generateCodeVerifier(): string {
2
- const array = new Uint8Array(32);
3
- crypto.getRandomValues(array);
4
- return btoa(String.fromCharCode(...array))
5
- .replace(/\+/g, "-")
6
- .replace(/\//g, "_")
7
- .replace(/=/g, "");
8
- }
9
-
10
- export async function generateCodeChallenge(verifier: string): Promise<string> {
11
- const encoder = new TextEncoder();
12
- const data = encoder.encode(verifier);
13
- const digest = await crypto.subtle.digest("SHA-256", data);
14
- return btoa(String.fromCharCode(...new Uint8Array(digest)))
15
- .replace(/\+/g, "-")
16
- .replace(/\//g, "_")
17
- .replace(/=/g, "");
18
- }
package/src/session.ts DELETED
@@ -1,21 +0,0 @@
1
- import { EncryptJWT, jwtDecrypt } from "jose";
2
-
3
- const getSecretKey = (secret: string) =>
4
- new TextEncoder().encode(secret.padEnd(32, "0").slice(0, 32));
5
-
6
- export async function encrypt(payload: any, secret: string) {
7
- return new EncryptJWT(payload)
8
- .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
9
- .setIssuedAt()
10
- .setExpirationTime("24h")
11
- .encrypt(getSecretKey(secret));
12
- }
13
-
14
- export async function decrypt(token: string, secret: string) {
15
- try {
16
- const { payload } = await jwtDecrypt(token, getSecretKey(secret));
17
- return payload;
18
- } catch (e) {
19
- return null;
20
- }
21
- }
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["DOM", "DOM.Iterable", "ESNext"],
7
- "jsx": "react-jsx",
8
- "declaration": true,
9
- "declarationMap": true,
10
- "sourceMap": true,
11
- "outDir": "./dist",
12
- "isolatedModules": true,
13
- "esModuleInterop": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "strict": true,
16
- "skipLibCheck": true,
17
- "types": ["node"]
18
- },
19
- "include": ["src"],
20
- "exclude": ["node_modules", "dist"]
21
- }
package/tsup.config.ts DELETED
@@ -1,18 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.tsx"],
5
- format: ["cjs", "esm"],
6
- dts: true,
7
- splitting: false,
8
- sourcemap: true,
9
- clean: true,
10
- minify: true,
11
- bundle: true,
12
- external: ["react", "next"],
13
- outExtension({ format }) {
14
- return {
15
- js: format === "esm" ? ".mjs" : ".cjs",
16
- };
17
- },
18
- });