@orsetra/shared-auth 1.0.0

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.
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import { ReactNode } from "react"
4
+ import { useZitadel } from "./ZitadelProvider"
5
+ import { useRouter } from "next/navigation"
6
+
7
+ interface ProtectedRouteProps {
8
+ children: ReactNode
9
+ /**
10
+ * URL de redirection si l'utilisateur n'est pas authentifié
11
+ * Par défaut: affiche un écran de connexion
12
+ */
13
+ redirectTo?: string
14
+ /**
15
+ * Fonction de vérification personnalisée (ex: vérifier un rôle)
16
+ */
17
+ authorize?: (user: any) => boolean
18
+ /**
19
+ * Message personnalisé si l'autorisation échoue
20
+ */
21
+ unauthorizedMessage?: string
22
+ }
23
+
24
+ /**
25
+ * Composant pour protéger une route ou une section spécifique
26
+ *
27
+ * @example
28
+ * // Protection simple
29
+ * <ProtectedRoute>
30
+ * <AdminPanel />
31
+ * </ProtectedRoute>
32
+ *
33
+ * @example
34
+ * // Avec vérification de rôle
35
+ * <ProtectedRoute
36
+ * authorize={(user) => user.profile?.roles?.includes('admin')}
37
+ * unauthorizedMessage="Accès réservé aux administrateurs"
38
+ * >
39
+ * <AdminPanel />
40
+ * </ProtectedRoute>
41
+ *
42
+ * @example
43
+ * // Avec redirection
44
+ * <ProtectedRoute redirectTo="/login">
45
+ * <Dashboard />
46
+ * </ProtectedRoute>
47
+ */
48
+ export function ProtectedRoute({
49
+ children,
50
+ redirectTo,
51
+ authorize,
52
+ unauthorizedMessage = "Vous n'avez pas les permissions nécessaires pour accéder à cette page.",
53
+ }: ProtectedRouteProps) {
54
+ const { user, isLoading, signIn } = useZitadel()
55
+ const router = useRouter()
56
+
57
+ // Chargement
58
+ if (isLoading) {
59
+ return (
60
+ <div className="flex items-center justify-center min-h-[400px]">
61
+ <div className="text-center">
62
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto mb-4"></div>
63
+ <p className="text-gray-600">Vérification des permissions...</p>
64
+ </div>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ // Non authentifié
70
+ if (!user) {
71
+ if (redirectTo) {
72
+ router.push(redirectTo)
73
+ return null
74
+ }
75
+
76
+ return (
77
+ <div className="flex items-center justify-center min-h-[400px] bg-gray-50">
78
+ <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
79
+ <h2 className="text-xl font-bold mb-4">Authentification requise</h2>
80
+ <p className="text-gray-600 mb-6">
81
+ Vous devez être connecté pour accéder à cette section.
82
+ </p>
83
+ <button
84
+ onClick={signIn}
85
+ className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
86
+ >
87
+ Se connecter
88
+ </button>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ // Vérification d'autorisation personnalisée
95
+ if (authorize && !authorize(user)) {
96
+ return (
97
+ <div className="flex items-center justify-center min-h-[400px] bg-gray-50">
98
+ <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
99
+ <div className="text-red-500 text-5xl mb-4">🔒</div>
100
+ <h2 className="text-xl font-bold mb-4">Accès refusé</h2>
101
+ <p className="text-gray-600">{unauthorizedMessage}</p>
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ // Autorisé
108
+ return <>{children}</>
109
+ }
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @orsetra/shared-auth
2
+
3
+ Shared authentication utilities for Orsetra platform using Zitadel OIDC.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @orsetra/shared-auth
9
+ # or
10
+ pnpm add @orsetra/shared-auth
11
+ ```
12
+
13
+ ## Peer Dependencies
14
+
15
+ ```bash
16
+ npm install react react-dom next oidc-client-ts jose
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### 1. Configuration Zitadel
22
+
23
+ ```typescript
24
+ import { createAuthConfig } from '@orsetra/shared-auth/config'
25
+
26
+ const authConfig = createAuthConfig({
27
+ authority: process.env.NEXT_PUBLIC_ZITADEL_AUTHORITY,
28
+ client_id: process.env.NEXT_PUBLIC_ZITADEL_CLIENT_ID,
29
+ project_resource_id: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID,
30
+ })
31
+ ```
32
+
33
+ ### 2. Provider dans l'App Principale
34
+
35
+ ```tsx
36
+ import { ZitadelProvider } from '@orsetra/shared-auth'
37
+
38
+ export default function RootLayout({ children }) {
39
+ return (
40
+ <html>
41
+ <body>
42
+ <ZitadelProvider config={authConfig}>
43
+ {children}
44
+ </ZitadelProvider>
45
+ </body>
46
+ </html>
47
+ )
48
+ }
49
+ ```
50
+
51
+ ### 3. Protected Routes
52
+
53
+ ```tsx
54
+ import { ProtectedRoute } from '@orsetra/shared-auth'
55
+
56
+ export default function DashboardPage() {
57
+ return (
58
+ <ProtectedRoute>
59
+ <Dashboard />
60
+ </ProtectedRoute>
61
+ )
62
+ }
63
+ ```
64
+
65
+ ### 4. Utiliser le Service d'Auth
66
+
67
+ ```typescript
68
+ import { ZitadelAuthService } from '@orsetra/shared-auth/services'
69
+
70
+ const authService = new ZitadelAuthService(authConfig)
71
+
72
+ // Login
73
+ await authService.login()
74
+
75
+ // Logout
76
+ await authService.logout()
77
+
78
+ // Get user
79
+ const user = await authService.getUser()
80
+
81
+ // Get access token
82
+ const token = await authService.getAccessToken()
83
+ ```
84
+
85
+ ## Architecture Micro-Frontend
86
+
87
+ ### App Main (Authentification Centralisée)
88
+
89
+ L'app `main` gère le flow OAuth :
90
+
91
+ ```tsx
92
+ // apps/main/app/layout.tsx
93
+ import { ZitadelProvider } from '@orsetra/shared-auth'
94
+
95
+ export default function RootLayout({ children }) {
96
+ return (
97
+ <ZitadelProvider config={authConfig}>
98
+ {children}
99
+ </ZitadelProvider>
100
+ )
101
+ }
102
+ ```
103
+
104
+ ### Micro-Apps (Validation de Token)
105
+
106
+ Les micro-apps valident le token reçu :
107
+
108
+ ```tsx
109
+ // apps/assets/middleware.ts
110
+ import { verifyToken } from '@orsetra/shared-auth/utils'
111
+
112
+ export async function middleware(request: NextRequest) {
113
+ const token = request.headers.get('x-auth-token')
114
+
115
+ if (!token) {
116
+ return NextResponse.redirect('/login')
117
+ }
118
+
119
+ const isValid = await verifyToken(token)
120
+
121
+ if (!isValid) {
122
+ return NextResponse.redirect('/login')
123
+ }
124
+
125
+ return NextResponse.next()
126
+ }
127
+ ```
128
+
129
+ ## Environment Variables
130
+
131
+ ```env
132
+ NEXT_PUBLIC_ZITADEL_AUTHORITY=https://your-instance.zitadel.cloud
133
+ NEXT_PUBLIC_ZITADEL_CLIENT_ID=your-client-id
134
+ NEXT_PUBLIC_ZITADEL_PROJECT_ID=your-project-id
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ## Repository
142
+
143
+ [GitHub](https://github.com/orsetra/console-ui/tree/main/packages/shared-auth)
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext, useEffect, useState, ReactNode } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { ZitadelAuthService, ZitadelConfig } from "./services/zitadel.auth.service"
6
+ import { AuthCard, AuthCardTitle, AuthCardDescription, AuthCardSpinner } from "./components/AuthCard"
7
+ import type { User } from "oidc-client-ts"
8
+
9
+ interface ZitadelContextType {
10
+ user: User | null
11
+ isLoading: boolean
12
+ isConfigured: boolean
13
+ login: () => Promise<void>
14
+ logout: () => Promise<void>
15
+ signIn: () => Promise<void>
16
+ signOut: () => Promise<void>
17
+ handleCallback: () => Promise<void>
18
+ refreshUser: () => Promise<void>
19
+ }
20
+
21
+ const ZitadelContext = createContext<ZitadelContextType | undefined>(undefined)
22
+
23
+ interface ZitadelProviderProps {
24
+ children: ReactNode
25
+ config: ZitadelConfig
26
+ }
27
+
28
+ export function ZitadelProvider({ children, config }: ZitadelProviderProps) {
29
+ const router = useRouter()
30
+ const [isConfigured, setIsConfigured] = useState(false)
31
+ const [isLoading, setIsLoading] = useState(true)
32
+ const [user, setUser] = useState<User | null>(null)
33
+
34
+ useEffect(() => {
35
+ const configure = async () => {
36
+ try {
37
+ ZitadelAuthService.configureAuth(config)
38
+ setIsConfigured(true)
39
+
40
+ const currentUser = await ZitadelAuthService.getUser()
41
+ setUser(currentUser)
42
+ } catch (error) {
43
+ console.error("Failed to configure Zitadel auth:", error)
44
+ } finally {
45
+ setIsLoading(false)
46
+ }
47
+ }
48
+
49
+ configure()
50
+ }, [config])
51
+
52
+ const signIn = async () => {
53
+ await ZitadelAuthService.signIn()
54
+ }
55
+
56
+ const signOut = async () => {
57
+ await ZitadelAuthService.signOut()
58
+ setUser(null)
59
+ }
60
+
61
+ const handleCallback = async () => {
62
+ try {
63
+ await ZitadelAuthService.handleCallback()
64
+ const currentUser = await ZitadelAuthService.getUser()
65
+ setUser(currentUser)
66
+ } catch (error) {
67
+ console.error('Callback handling failed:', error)
68
+ throw error
69
+ }
70
+ }
71
+
72
+ const refreshUser = async () => {
73
+ const currentUser = await ZitadelAuthService.getUser()
74
+ setUser(currentUser)
75
+ }
76
+
77
+ // Aliases pour compatibilité
78
+ const login = signIn
79
+ const logout = signOut
80
+
81
+ useEffect(() => {
82
+ if (!isLoading && !user && isConfigured) {
83
+ signIn()
84
+ }
85
+ }, [isLoading, user, isConfigured])
86
+
87
+ const value: ZitadelContextType = {
88
+ user,
89
+ isLoading,
90
+ isConfigured,
91
+ login,
92
+ logout,
93
+ signIn,
94
+ signOut,
95
+ handleCallback,
96
+ refreshUser,
97
+ }
98
+
99
+ if (isLoading) {
100
+ return (
101
+ <ZitadelContext.Provider value={value}>
102
+ <AuthCard>
103
+ <div className="text-center py-8">
104
+ <AuthCardSpinner />
105
+ <AuthCardTitle>Chargement</AuthCardTitle>
106
+ <AuthCardDescription>Vérification de l'authentification...</AuthCardDescription>
107
+ </div>
108
+ </AuthCard>
109
+ </ZitadelContext.Provider>
110
+ )
111
+ }
112
+
113
+ if (!user) {
114
+ return (
115
+ <ZitadelContext.Provider value={value}>
116
+ <AuthCard>
117
+ <div className="text-center py-8">
118
+ <AuthCardSpinner />
119
+ <AuthCardTitle>Redirection</AuthCardTitle>
120
+ <AuthCardDescription>Veuillez patienter...</AuthCardDescription>
121
+ </div>
122
+ </AuthCard>
123
+ </ZitadelContext.Provider>
124
+ )
125
+ }
126
+
127
+ return (
128
+ <ZitadelContext.Provider value={value}>
129
+ {children}
130
+ </ZitadelContext.Provider>
131
+ )
132
+ }
133
+
134
+ export function useZitadel() {
135
+ const context = useContext(ZitadelContext)
136
+ if (context === undefined) {
137
+ throw new Error("useZitadel must be used within a ZitadelProvider")
138
+ }
139
+ return context
140
+ }
@@ -0,0 +1,143 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useState } from "react"
4
+
5
+ interface AuthCardProps {
6
+ children: ReactNode
7
+ className?: string
8
+ }
9
+
10
+ /**
11
+ * Composant Card pour les pages d'authentification
12
+ * Utilise le même style que le login Zitadel (IBM Carbon Design)
13
+ */
14
+ export function AuthCard({ children, className = "" }: AuthCardProps) {
15
+ const [isDark, setIsDark] = useState(false)
16
+
17
+ useEffect(() => {
18
+ // Détecter le dark mode
19
+ const checkDarkMode = () => {
20
+ setIsDark(document.documentElement.classList.contains('dark'))
21
+ }
22
+
23
+ checkDarkMode()
24
+
25
+ // Observer les changements de thème
26
+ const observer = new MutationObserver(checkDarkMode)
27
+ observer.observe(document.documentElement, {
28
+ attributes: true,
29
+ attributeFilter: ['class']
30
+ })
31
+
32
+ return () => observer.disconnect()
33
+ }, [])
34
+
35
+ const bgColor = isDark ? 'var(--theme-dark-background-600)' : 'var(--theme-light-background-600)'
36
+ const cardBg = isDark ? 'var(--theme-dark-background-500)' : 'var(--theme-light-background-100)'
37
+ const borderColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
38
+
39
+ return (
40
+ <div className="relative flex min-h-screen flex-col justify-center" style={{ backgroundColor: bgColor }}>
41
+ <div className="relative mx-auto py-8 px-4" style={{ width: '440px' }}>
42
+ <div
43
+ className={`p-8 ${className}`}
44
+ style={{
45
+ backgroundColor: cardBg,
46
+ border: '1px solid',
47
+ borderColor: borderColor,
48
+ borderRadius: '0',
49
+ width: '100%',
50
+ height: '450px',
51
+ boxShadow: 'none',
52
+ display: 'flex',
53
+ flexDirection: 'column',
54
+ justifyContent: 'center',
55
+ overflow: 'hidden'
56
+ }}
57
+ >
58
+ {children}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ interface AuthCardTitleProps {
66
+ children: ReactNode
67
+ }
68
+
69
+ export function AuthCardTitle({ children }: AuthCardTitleProps) {
70
+ const [isDark, setIsDark] = useState(false)
71
+
72
+ useEffect(() => {
73
+ const checkDarkMode = () => setIsDark(document.documentElement.classList.contains('dark'))
74
+ checkDarkMode()
75
+ const observer = new MutationObserver(checkDarkMode)
76
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
77
+ return () => observer.disconnect()
78
+ }, [])
79
+
80
+ return (
81
+ <h2
82
+ className="text-xl font-semibold mb-3 text-center"
83
+ style={{ color: isDark ? 'var(--theme-dark-text)' : 'var(--theme-light-text)' }}
84
+ >
85
+ {children}
86
+ </h2>
87
+ )
88
+ }
89
+
90
+ interface AuthCardDescriptionProps {
91
+ children: ReactNode
92
+ }
93
+
94
+ export function AuthCardDescription({ children }: AuthCardDescriptionProps) {
95
+ const [isDark, setIsDark] = useState(false)
96
+
97
+ useEffect(() => {
98
+ const checkDarkMode = () => setIsDark(document.documentElement.classList.contains('dark'))
99
+ checkDarkMode()
100
+ const observer = new MutationObserver(checkDarkMode)
101
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
102
+ return () => observer.disconnect()
103
+ }, [])
104
+
105
+ return (
106
+ <p
107
+ className="text-center leading-relaxed"
108
+ style={{ color: isDark ? 'var(--theme-dark-secondary-text)' : 'var(--theme-light-secondary-text)' }}
109
+ >
110
+ {children}
111
+ </p>
112
+ )
113
+ }
114
+
115
+ interface AuthCardSpinnerProps {
116
+ className?: string
117
+ }
118
+
119
+ export function AuthCardSpinner({ className = "" }: AuthCardSpinnerProps) {
120
+ const [isDark, setIsDark] = useState(false)
121
+
122
+ useEffect(() => {
123
+ const checkDarkMode = () => setIsDark(document.documentElement.classList.contains('dark'))
124
+ checkDarkMode()
125
+ const observer = new MutationObserver(checkDarkMode)
126
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
127
+ return () => observer.disconnect()
128
+ }, [])
129
+
130
+ const spinnerColor = isDark ? 'var(--theme-dark-primary-500)' : 'var(--theme-light-primary-500)'
131
+
132
+ return (
133
+ <div
134
+ className={`animate-spin rounded-full h-16 w-16 mx-auto mb-6 ${className}`}
135
+ style={{
136
+ borderWidth: '4px',
137
+ borderStyle: 'solid',
138
+ borderColor: 'transparent',
139
+ borderBottomColor: spinnerColor
140
+ }}
141
+ />
142
+ )
143
+ }
@@ -0,0 +1,43 @@
1
+ import { UserManagerSettings } from 'oidc-client-ts';
2
+
3
+ export type ZitadelConfig = Partial<UserManagerSettings> & {
4
+ project_resource_id?: string;
5
+ };
6
+
7
+ /**
8
+ * Obtient l'URL de base de l'application selon l'environnement
9
+ * - Production: URL Vercel ou domaine personnalisé
10
+ * - Development: localhost:3000
11
+ */
12
+ function getBaseUrl(): string {
13
+ // En production, utiliser l'URL de l'application
14
+ if (typeof window !== 'undefined') {
15
+ return window.location.origin;
16
+ }
17
+
18
+ return '';
19
+ }
20
+
21
+ /**
22
+ * Crée la configuration UserManager à partir de la config Zitadel
23
+ */
24
+ export function createAuthConfig(zitadelConfig: ZitadelConfig): UserManagerSettings {
25
+ const baseUrl = getBaseUrl();
26
+
27
+ return {
28
+ authority: `${zitadelConfig.authority}`,
29
+ client_id: `${zitadelConfig.client_id}`,
30
+ redirect_uri: zitadelConfig.redirect_uri ?? `${baseUrl}/callback`,
31
+ response_type: zitadelConfig.response_type ?? 'code',
32
+ scope: zitadelConfig.scope ?? `openid profile email ${
33
+ zitadelConfig.project_resource_id
34
+ ? `urn:zitadel:iam:org:project:id:${zitadelConfig.project_resource_id}:aud urn:zitadel:iam:org:projects:roles`
35
+ : ''
36
+ }`,
37
+ prompt: zitadelConfig.prompt ?? '',
38
+ post_logout_redirect_uri: zitadelConfig.post_logout_redirect_uri ?? baseUrl,
39
+ response_mode: zitadelConfig.response_mode ?? 'query',
40
+ disablePKCE: zitadelConfig.disablePKCE,
41
+ extraQueryParams: zitadelConfig.extraQueryParams,
42
+ };
43
+ }
package/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Auth exports
2
+ export { ZitadelProvider, useZitadel } from './ZitadelProvider'
3
+ export { ProtectedRoute } from './ProtectedRoute'
4
+ export { ZitadelAuthService } from './services/zitadel.auth.service'
5
+ export { createAuthConfig } from './config/zitadel.config'
6
+ export type { ZitadelConfig } from './config/zitadel.config'
7
+ export * from './utils'
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@orsetra/shared-auth",
3
+ "version": "1.0.0",
4
+ "description": "Shared authentication utilities for Orsetra platform using Zitadel",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./config": "./config/zitadel.config.ts",
10
+ "./services": "./services/zitadel.auth.service.ts",
11
+ "./components": "./components/index.ts",
12
+ "./utils": "./utils/redirect-utils.ts"
13
+ },
14
+ "files": [
15
+ "index.ts",
16
+ "ZitadelProvider.tsx",
17
+ "ProtectedRoute.tsx",
18
+ "config",
19
+ "services",
20
+ "components",
21
+ "utils",
22
+ "README.md"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/orsetra/console-ui.git",
30
+ "directory": "packages/shared-auth"
31
+ },
32
+ "keywords": [
33
+ "authentication",
34
+ "zitadel",
35
+ "oidc",
36
+ "oauth2",
37
+ "orsetra"
38
+ ],
39
+ "peerDependencies": {
40
+ "react": "^18.0.0 || ^19.0.0",
41
+ "react-dom": "^18.0.0 || ^19.0.0",
42
+ "next": "^14.0.0 || ^15.0.0"
43
+ },
44
+ "dependencies": {
45
+ "oidc-client-ts": "^3.2.1",
46
+ "jose": "^5.9.6"
47
+ },
48
+ "devDependencies": {
49
+ "@types/react": "^19",
50
+ "typescript": "^5"
51
+ }
52
+ }
@@ -0,0 +1,93 @@
1
+ "use client"
2
+
3
+ import { User, UserManager, WebStorageStateStore } from 'oidc-client-ts';
4
+ import { ZitadelConfig, createAuthConfig } from '../config/zitadel.config';
5
+
6
+ export type { ZitadelConfig };
7
+
8
+ export interface ZitadelAuth {
9
+ authorize(): Promise<void>;
10
+ signout(): Promise<void>;
11
+ userManager: UserManager;
12
+ }
13
+
14
+ function createZitadelAuth(zitadelConfig: ZitadelConfig): ZitadelAuth {
15
+ const authConfig = createAuthConfig(zitadelConfig);
16
+
17
+ const userManager = new UserManager({
18
+ userStore: new WebStorageStateStore({ store: window.localStorage }),
19
+ loadUserInfo: true,
20
+ ...authConfig,
21
+ });
22
+
23
+ return {
24
+ authorize: () => userManager.signinRedirect(),
25
+ signout: () => userManager.signoutRedirect(),
26
+ userManager,
27
+ };
28
+ }
29
+
30
+ export class ZitadelAuthService {
31
+ private static zitadelAuth: ZitadelAuth | null = null;
32
+
33
+ static configureAuth(config: ZitadelConfig) {
34
+ this.zitadelAuth = createZitadelAuth(config);
35
+ }
36
+
37
+ private static getAuth(): ZitadelAuth {
38
+ if (!this.zitadelAuth) {
39
+ throw new Error('Zitadel auth not configured. Call configureAuth() first.');
40
+ }
41
+ return this.zitadelAuth;
42
+ }
43
+
44
+ static async signIn() {
45
+ try {
46
+ await this.getAuth().authorize();
47
+ } catch (error) {
48
+ throw this.handleAuthError(error);
49
+ }
50
+ }
51
+
52
+ static async signOut() {
53
+ try {
54
+ await this.getAuth().signout();
55
+ } catch (error) {
56
+ throw this.handleAuthError(error);
57
+ }
58
+ }
59
+
60
+ static async getUser(): Promise<User | null> {
61
+ try {
62
+ return await this.getAuth().userManager.getUser();
63
+ } catch (error) {
64
+ console.error('Error getting user:', error);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ static async handleCallback(): Promise<User> {
70
+ try {
71
+ return await this.getAuth().userManager.signinRedirectCallback();
72
+ } catch (error) {
73
+ throw this.handleAuthError(error);
74
+ }
75
+ }
76
+
77
+ static async isAuthenticated(): Promise<boolean> {
78
+ const user = await this.getUser();
79
+ return user !== null && !user.expired;
80
+ }
81
+
82
+ static getUserManager(): UserManager {
83
+ return this.getAuth().userManager;
84
+ }
85
+
86
+ private static handleAuthError(error: any): Error {
87
+ console.error('Auth error:', error);
88
+ if (error.message) {
89
+ return new Error(error.message);
90
+ }
91
+ return new Error('An error occurred during authentication');
92
+ }
93
+ }
package/utils/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './token-validator'
2
+ export * from '../redirect-utils'
@@ -0,0 +1,54 @@
1
+ import { jwtVerify, createRemoteJWKSet } from 'jose'
2
+
3
+ /**
4
+ * Vérifie et décode un token JWT Zitadel
5
+ */
6
+ export async function verifyToken(
7
+ token: string,
8
+ authority: string,
9
+ clientId: string
10
+ ): Promise<boolean> {
11
+ try {
12
+ const JWKS = createRemoteJWKSet(new URL(`${authority}/.well-known/openid-configuration/jwks`))
13
+
14
+ const { payload } = await jwtVerify(token, JWKS, {
15
+ issuer: authority,
16
+ audience: clientId,
17
+ })
18
+
19
+ // Vérifier l'expiration
20
+ if (payload.exp && payload.exp < Date.now() / 1000) {
21
+ return false
22
+ }
23
+
24
+ return true
25
+ } catch (error) {
26
+ console.error('Token validation failed:', error)
27
+ return false
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Décode un token JWT sans vérification (pour debug uniquement)
33
+ */
34
+ export function decodeToken(token: string): any {
35
+ try {
36
+ const [, payload] = token.split('.')
37
+ return JSON.parse(Buffer.from(payload, 'base64').toString())
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Extrait le token du header Authorization
45
+ */
46
+ export function extractTokenFromHeader(authHeader: string | null): string | null {
47
+ if (!authHeader) return null
48
+
49
+ const [type, token] = authHeader.split(' ')
50
+
51
+ if (type !== 'Bearer') return null
52
+
53
+ return token
54
+ }