Skip to content

GeospatialEnablingTechnologies/adonis-js-keycloak-driver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Keycloak Driver για AdonisJS Ally

Ένας OAuth2 driver για το Keycloak που λειτουργεί με το AdonisJS Ally v7.

Εγκατάσταση

npm install git@github.com:GeospatialEnablingTechnologies/adonis-js-keycloak-driver.git

Απεγκατάσταση

npm uninstall adonis-ally-keycloak

Ρύθμιση

1. Ρύθμιση Περιβαλλοντικών Μεταβλητών

Πρόσθεσε τα παρακάτω στο .env αρχείο σου:

KEYCLOAK_SERVER_URL=https://your-keycloak-server.com
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret
KEYCLOAK_CALLBACK_URL=http://localhost:3333/auth/keycloak/callback

2. Ρύθμιση του Ally Config

Άνοιξε το config/ally.ts και πρόσθεσε τον Keycloak driver:

import env from '#start/env'
import { defineConfig } from '@adonisjs/ally'
import type { InferSocialProviders } from '@adonisjs/ally/types'
import { KeycloakService } from 'adonis-ally-keycloak'

const allyConfig = defineConfig({
  keycloak: KeycloakService({
    serverUrl: env.get('KEYCLOAK_SERVER_URL'),
    realm: env.get('KEYCLOAK_REALM'),
    clientId: env.get('KEYCLOAK_CLIENT_ID'),
    clientSecret: env.get('KEYCLOAK_CLIENT_SECRET'),
    callbackUrl: env.get('KEYCLOAK_CALLBACK_URL'),
  }),
})

export default allyConfig

declare module '@adonisjs/ally/types' {
  interface SocialProviders extends InferSocialProviders<typeof allyConfig> {}
}

Χρήση

Controller

import type { HttpContext } from '@adonisjs/core/http'

export default class AuthController {
  /**
   * Redirect to Keycloak for authentication
   * GET /auth/keycloak
   */
  async redirect({ ally, response }: HttpContext) {
    const keycloak = ally.use('keycloak')
    const url = await keycloak.redirectUrl()
    return response.redirect(url)
  }

  /**
   * Handle Keycloak callback
   * GET /auth/keycloak/callback
   */
  async callback({ ally, session, request, response }: HttpContext) {

    const keycloak = ally.use('keycloak')

    // Check state
    if (keycloak.stateMisMatch()) {
      const cookieState = request.cookie('keycloak_oauth_state')
      const queryState = request.qs().state
      return response.status(403).json({
        error: 'State mismatch',
        debug: {
          cookieState,
          queryState,
          sessionId: session.sessionId,
        }
      })
    }

    // Check if user denied access
    if (keycloak.accessDenied()) {
      return response.status(401).json({
        error: 'Access denied',
        message: 'User denied access to the application',
      })
    }

    // Check for state mismatch (CSRF protection)
    if (keycloak.stateMisMatch()) {
      return response.status(403).json({
        error: 'State mismatch',
        message: 'Invalid state parameter',
      })
    }

    // Check for any other errors
    if (keycloak.hasError()) {
      return response.status(500).json({
        error: 'Authentication error',
        message: keycloak.getError(),
      })
    }

    try {
      // Get user from Keycloak
      const keycloakUser = await keycloak.user()

      // Store user in session
      session.put('user', {
        id: keycloakUser.id,
        email: keycloakUser.email,
        name: keycloakUser.name,
        emailVerified: keycloakUser.emailVerificationState === 'verified',
        avatarUrl: keycloakUser.avatarUrl,
      })

      // Store access token (optional - για API calls)
      session.put('keycloak_token', keycloakUser.token.token)

      if (keycloakUser.token.refreshToken) {
        session.put('keycloak_refresh_token', keycloakUser.token.refreshToken)
      }

      // Redirect to dashboard ή home page
      return response.redirect('/')
    } catch (error) {
      console.error('Keycloak authentication failed:', error)

      return response.status(500).json({
        error: 'Authentication failed',
        message: 'Failed to authenticate with Keycloak',
      })
    }
  }

  /**
   * Get current authenticated user
   * GET /auth/me
   */
  async me({ session, response }: HttpContext) {
    const user = session.get('user')

    if (!user) {
      return response.status(401).json({
        error: 'Not authenticated',
        message: 'User is not logged in',
      })
    }

    return response.json({
      user,
    })
  }

  /**
   * Logout from Keycloak and application
   * GET /auth/logout
   */
  async logout({ ally, session, response }: HttpContext) {
    const keycloak = ally.use('keycloak')

    // Clear local session
    await session.clear()

    // Get Keycloak logout URL
    const logoutUrl = keycloak.getLogoutUrl('http://localhost:3333')

    // Redirect to Keycloak logout (this logs out from Keycloak SSO)
    return response.redirect(logoutUrl)
  }

  /**
   * Simple local logout (doesn't logout from Keycloak SSO)
   * POST /auth/logout/local
   */
  async logoutLocal({ session, response }: HttpContext) {
    await session.clear()
    return response.json({
      message: 'Logged out successfully',
    })
  }
}

Προσαρμοσμένα Scopes

Μπορείς να ορίσεις scopes με δύο τρόπους:

1. Στο configuration (default για όλα τα redirects):

// config/ally.ts
export default defineConfig({
  keycloak: KeycloakService({
    serverUrl: env.get('KEYCLOAK_SERVER_URL'),
    realm: env.get('KEYCLOAK_REALM'),
    clientId: env.get('KEYCLOAK_CLIENT_ID'),
    clientSecret: env.get('KEYCLOAK_CLIENT_SECRET'),
    callbackUrl: env.get('KEYCLOAK_CALLBACK_URL'),
    scopes: ['openid', 'profile', 'email', 'roles'], // Default scopes
  }),
})

2. Κατά το redirect (override τα default):

router.get('/auth/keycloak', async ({ ally }: HttpContext) => {
  return ally
    .use('keycloak')
    .scopes(['openid', 'profile', 'email', 'roles'])
    .redirect()
})

Αν δεν ορίσεις scopes, θα χρησιμοποιηθούν τα default: ['openid', 'profile', 'email']

Πρόσθετα Παράμετροι κατά το Redirect

router.get('/auth/keycloak', async ({ ally }: HttpContext) => {
  return ally
    .use('keycloak')
    .redirect((request) => {
      // Προσθήκη προσαρμοσμένων παραμέτρων
      request.param('kc_idp_hint', 'google')
      // Ή για login hint
      request.param('login_hint', 'user@example.com')
    })
})

Logout από Keycloak

router.get('/logout', async ({ ally, response, session }: HttpContext) => {
  const keycloak = ally.use('keycloak')
  
  // Καθαρισμός local session
  await session.clear()
  
  // Λήψη Keycloak logout URL
  const logoutUrl = keycloak.getLogoutUrl('http://localhost:3333')
  
  // Redirect στο Keycloak για logout
  return response.redirect(logoutUrl)
})

User Object Structure

Ο driver επιστρέφει ένα αντικείμενο χρήστη που υλοποιεί το AllyUserContract:

{
  id: string                    // Keycloak user ID (sub)
  nickName: string              // Preferred username ή name
  name: string                  // Full name
  email: string                 // Email address
  emailVerificationState: 'verified' | 'unverified'
  avatarUrl: string | null      // Profile picture URL (αν υπάρχει)
  token: {                      // Access token information
    token: string               // The access token
    type: 'bearer'              // Token type
    refreshToken?: string       // Refresh token (αν υπάρχει)
    expiresIn?: number         // Expiration time in seconds
    expiresAt?: Date           // Expiration timestamp
  }
  original: {                   // Πλήρες Keycloak user object
    sub: string
    email_verified: boolean
    name: string
    preferred_username: string
    given_name?: string
    family_name?: string
    email: string
    picture?: string
    // ... όλα τα πεδία από το Keycloak
  }
}

Keycloak Client Configuration

Για να λειτουργήσει ο driver, πρέπει να ρυθμίσεις έναν OAuth2 client στο Keycloak:

  1. Άνοιξε το Keycloak Admin Console
  2. Επίλεξε το realm σου
  3. Πήγαινε στο Clients > Create Client
  4. Ρύθμισε:
    • Client ID: Το client ID που θα χρησιμοποιήσεις
    • Client Protocol: openid-connect
    • Access Type: confidential
    • Valid Redirect URIs: http://localhost:3333/auth/keycloak/callback (ή το δικό σου URL)
    • Web Origins: http://localhost:3333 (ή το δικό σου domain)
  5. Αντίγραψε το Client Secret από το Credentials tab

Διαθέσιμα Scopes

Τα πιο κοινά Keycloak scopes:

  • openid - Βασικό OpenID Connect scope (απαραίτητο)
  • profile - Βασικά στοιχεία προφίλ (name, username, κτλ.)
  • email - Email address και email verification status
  • address - Διεύθυνση χρήστη
  • phone - Τηλέφωνο χρήστη
  • offline_access - Refresh token για offline πρόσβαση
  • roles - Keycloak roles
  • Custom scopes που έχεις ορίσει στο Keycloak

Error Handling

router.get('/auth/keycloak/callback', async ({ ally, response }: HttpContext) => {
  const keycloak = ally.use('keycloak')

  if (keycloak.accessDenied()) {
    return response.status(401).send('Ο χρήστης αρνήθηκε την πρόσβαση')
  }

  if (keycloak.stateMisMatch()) {
    return response.status(403).send('State mismatch error')
  }

  if (keycloak.hasError()) {
    const error = keycloak.getError()
    return response.status(500).send(`Authentication error: ${error}`)
  }

  try {
    const user = await keycloak.user()
    // ...
  } catch (error) {
    return response.status(500).send('Failed to fetch user')
  }
})

Advanced Usage

Χρήση με Middleware

// app/middleware/auth_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class AuthMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    // Έλεγχος αν ο χρήστης είναι authenticated
    // ...
    
    await next()
  }
}

Αποθήκευση Access Token

router.get('/auth/keycloak/callback', async ({ ally, session, response }: HttpContext) => {
  const keycloak = ally.use('keycloak')
  
  // Λήψη access token
  const accessToken = await keycloak.accessToken()
  
  // Αποθήκευση στο session
  session.put('keycloak_token', accessToken.token)
  
  if (accessToken.refreshToken) {
    session.put('keycloak_refresh_token', accessToken.refreshToken)
  }
  
  // Λήψη user
  const user = await keycloak.user()
  
  // ...
})

License

MIT

Support

Για issues και questions, παρακαλώ άνοιξε ένα issue στο GitHub repository.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages