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

No releases published

Packages

 
 
 

Contributors