AdonisJS Ally

repository·6.x·Indexed 41 days ago

https://github.com/adonisjs/ally

A social authentication provider for the AdonisJS framework (version 6.3.0) that provides a unified interface for integrating multiple OAuth providers, including GitHub, Google, Twitter, X, Facebook, Discord, Spotify, and LinkedIn.

Tokens
11.8K
Snippets
41
Records
61
Agent score
50%

What's inside Ally

  1. Introduction to @adonisjs/ally

    6.x

    Ally is a social authentication provider package for AdonisJS. It allows you to integrate social login capabilities into your application using various third-party providers.

    Supported providers include:

    • Github
    • Google
    • Twitter
    • X (Twitter OAuth2)
    • Facebook
    • Discord
    • Spotify
    • LinkedIn
  2. Verify user email trust signals across OAuth providers

    6.x

    When using Ally to implement social authentication, you must decide whether to trust the email address returned by the provider as a 'verified' email. Different providers offer different levels of assurance through their OIDC claims or user payloads. Use the following trust signals to guide your implementation logic:

    High Trust (Verified)

    • google: Returns an explicit email_verified claim in OIDC. This is considered a reliable trust signal.
    • discord: The user payload includes a verified field for the user email. Note: Always verify the exact field name via the Ally driver payload before relying on it.
    • github: Can be treated as verified only if your integration is explicitly receiving verified email information from GitHub's user/email APIs. Otherwise, use the GitHub ID as the primary identity.

    Low or No Trust (Do Not Use for Email Verification)

    • linkedin_openid_connect: LinkedIn explicitly states that Sign In with LinkedIn does not verify user identities.
    • spotify: The email returned from the /me endpoint is explicitly unverified.
    • facebook: No standard email-verification signal is reliably exposed in the normal login profile payload.
    • twitter/x: No official documentation confirms that the returned email carries a trustworthy verification-state claim.

    Recommendation: If a provider is marked as 'unsupported/unclear' or 'unverified', do not treat the email as proof of identity for sensitive operations.

  3. Configure Google driver scopes

    6.x

    The Google driver automatically prefixes short scope names with the appropriate Google API URL (e.g., userinfo.email becomes https://www.googleapis.com/auth/userinfo.email).

    Supported short scope names include:

    • userinfo.email
    • userinfo.profile
    • contacts
    • calendar.readonly
    • drive.readonly
    • spreadsheets
    • (and many others...)

    You can define default scopes in your driver configuration or override them during the redirect request using request.scopes([...]).

  4. Handle Ally authentication errors

    6.x

    Ally uses specialized error classes that extend HttpResponseException. These errors are designed to automatically handle HTTP responses based on content negotiation (HTML, JSON, or JSONAPI).

    When an error is caught, you can call the .handle(ctx) method on the error instance to automatically:

    • HTML: Flash the error message to the session and redirect the user back.
    • JSON: Return a JSON object with an errors array containing the message.
    • JSONAPI: Return a JSONAPI-compliant error object including the error code and title.

    If @adonisjs/i18n is configured, these errors will automatically attempt to translate the error message using their internal identifier.

    try {
      // ... authentication logic
    } catch (error) {
      if (error instanceof errors.HttpResponseException) {
        await error.handle(ctx)
      }
    }
  5. Configure interaction intents with AllyManager

    6.x

    When using a provider through AllyManager, you can specify a high-level intent to guide the authentication flow. This is useful for distinguishing between different user journeys like signing up for the first time versus logging in or linking an existing account.

    Supported intents:

    • signup
    • login
    • link
    // Example usage of intent
    ally.use('github', { intent: 'signup' })
  6. Authenticate users via Facebook

    6.x

    To implement Facebook social authentication, you need to handle two main steps: the redirect to Facebook for authorization and the callback to process the user information.

    1. Redirect: Use ally.use('facebook').redirect() to send the user to Facebook. You can customize the requested scopes using .scopes(['email', 'public_profile']).

    2. Callback: In your callback route, use the Facebook driver to check for errors (like access denial or state mismatch) before fetching the user profile with .user().

    Common error checks include:

    • accessDenied(): Returns true if the user explicitly denied the request.
    • stateMisMatch(): Returns true if the CSRF state does not match.
    • hasError(): Returns true if any error occurred during the process.
    router.get('/facebook/redirect', ({ ally }) => {
      return ally.use('facebook').redirect((request) => {
        request.scopes(['email', 'public_profile'])
      })
    })
    
    router.get('/facebook/callback', async ({ ally }) => {
      const facebook = ally.use('facebook')
    
      if (facebook.accessDenied()) {
        return 'Access was denied'
      }
    
      if (facebook.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (facebook.hasError()) {
        return facebook.getError()
      }
    
      const user = await facebook.user()
      return user
    })
  7. Implement GitHub social authentication

    6.x

    Use the GitHub driver via ally.use('github') to handle OAuth2 authentication flows. This involves two main steps: redirecting the user to GitHub and handling the callback to retrieve user information.

    1. Redirecting the user

    Use .redirect() to send the user to GitHub. You can customize the requested scopes using .scopes().

    2. Handling the callback

    In your callback route, use the driver to check for errors (like access denial or state mismatch) before fetching the user profile with .user().

    Common checks include:

    • accessDenied(): Returns true if the user rejected the authorization request.
    • stateMisMatch(): Returns true if the CSRF state does not match.
    • hasError(): Returns true if there is any error.
    • getError(): Returns the error string if hasError() is true.
    // 1. Redirecting to GitHub
    router.get('/github/redirect', ({ ally }) => {
      return ally.use('github').redirect((request) => {
        request.scopes(['user:email', 'read:org'])
      })
    })
    
    // 2. Handling the callback
    router.get('/github/callback', async ({ ally }) => {
      const github = ally.use('github')
    
      if (github.accessDenied()) {
        return 'Access was denied'
      }
    
      if (github.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (github.hasError()) {
        return github.getError()
      }
    
      const user = await github.user()
      return user
    })
  8. Authenticate users with Spotify via Ally

    6.x

    To implement Spotify social authentication, use the spotify driver via the ally object. The process involves two main steps: redirecting the user to Spotify for authorization and handling the callback to retrieve the user profile.

    1. Redirecting the user

    Use ally.use('spotify').redirect() to send the user to Spotify. You can customize the requested scopes using the .scopes() method on the request object.

    2. Handling the callback

    In your callback route, use the spotify driver to check for errors (like access denial or state mismatches) before fetching the user information with .user().

    User Profile Data

    The .user() method returns an object containing:

    • id: Spotify user ID
    • nickName: Spotify display name
    • name: Spotify display name
    • email: User email
    • avatarUrl: URL to the first profile image (or null)
    • token: The access token object
    • original: The raw response from the Spotify API
    // 1. Redirecting the user
    router.get('/spotify/redirect', ({ ally }) => {
      return ally.use('spotify').redirect((request) => {
        request.scopes(['user-read-email', 'user-read-private'])
      })
    })
    
    // 2. Handling the callback
    router.get('/spotify/callback', async ({ ally }) => {
      const spotify = ally.use('spotify')
    
      if (spotify.accessDenied()) {
        return 'Access was denied'
      }
    
      if (spotify.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (spotify.hasError()) {
        return spotify.getError()
      }
    
      const user = await spotify.user()
      return user
    })
  9. Authenticate users via Twitter using Ally

    6.x

    To authenticate users with Twitter, use the twitter driver via the ally service. You typically implement two routes: a redirect route to send the user to Twitter, and a callback route to handle the response and retrieve the user profile.

    Common checks in the callback route include accessDenied(), stateMisMatch(), and hasError() to ensure the authentication flow was successful and secure.

    router.get('/twitter/redirect', ({ ally }) => {
      return ally.use('twitter').redirect()
    })
    
    router.get('/twitter/callback', async ({ ally }) => {
      const twitter = ally.use('twitter')
    
      if (twitter.accessDenied()) {
        return 'Access was denied'
      }
    
      if (twitter.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (twitter.hasError()) {
        return twitter.getError()
      }
    
      const user = await twitter.user()
      return user
    })
  10. Authenticate users with LinkedIn

    6.x

    To implement LinkedIn social authentication, use the linkedin driver via the ally service. You typically need two routes: one to redirect the user to LinkedIn for authorization, and a callback route to handle the response and retrieve the user profile.

    1. Redirect to LinkedIn

    Use ally.use('linkedin').redirect() to send the user to LinkedIn. You can specify required scopes using .scopes(). Default scopes are ['r_emailaddress', 'r_liteprofile'].

    2. Handle the Callback

    In your callback route, check for common OAuth errors like access denial or state mismatches before calling .user() to fetch the profile and email.

    Note: This driver fetches the email address separately from the profile information. Ensure you have requested the r_emailaddress scope.

    // 1. Redirect route
    router.get('/linkedin/redirect', ({ ally }) => {
      return ally.use('linkedin').redirect((request) => {
        request.scopes(['r_emailaddress', 'r_liteprofile'])
      })
    })
    
    // 2. Callback route
    router.get('/linkedin/callback', async ({ ally }) => {
      const linkedin = ally.use('linkedin')
    
      if (linkedin.accessDenied()) {
        return 'Access was denied'
      }
    
      if (linkedin.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (linkedin.hasError()) {
        return linkedin.getError()
      }
    
      const user = await linkedin.user()
      return user
    })
  11. Use the Google driver for social authentication

    6.x

    The Google driver allows you to authenticate users via Google OAuth2. It supports fetching user profile information such as name, email, and profile picture. You can initiate a redirect to Google's authorization page and handle the callback to retrieve the user's profile.

    Redirecting to Google

    To start the authentication process, use ally.use('google').redirect(). You can customize the requested scopes within the redirect callback.

    Handling the Callback

    In your callback route, you should check for common OAuth errors like access denial, state mismatch, or other errors before attempting to fetch the user profile.

    Fetching the User

    Once the callback is successful, use google.user() to retrieve the user's profile and access token, or google.userFromToken(token) if you already have an access token.

    // 1. Redirect to Google
    router.get('/google/redirect', ({ ally }) => {
      return ally.use('google').redirect((request) => {
        request.scopes(['userinfo.email', 'userinfo.profile', 'calendar.readonly'])
      })
    })
    
    // 2. Handle Callback
    router.get('/google/callback', async ({ ally }) => {
      const google = ally.use('google')
    
      if (google.accessDenied()) {
        return 'Access was denied'
      }
    
      if (google.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (google.hasError()) {
        return google.getError()
      }
    
      const user = await google.user()
      return user
    })
  12. Authenticate users with LinkedIn OpenID Connect

    6.x

    Use the linkedinOpenidConnect driver to implement social authentication via LinkedIn's OpenID Connect protocol. This driver allows you to redirect users to LinkedIn for authorization and subsequently retrieve their profile information (name, email, avatar, etc.) upon successful callback.

    To use this driver, you must first configure it in your AdonisJS application and then use the ally.use('linkedinOpenidConnect') method within your routes.

    // 1. Redirecting the user to LinkedIn
    router.get('/linkedin/redirect', ({ ally }) => {
      return ally.use('linkedinOpenidConnect').redirect((request) => {
        // Optional: Define custom scopes. Defaults to ['openid', 'profile', 'email']
        request.scopes(['openid', 'profile', 'email'])
      })
    })
    
    // 2. Handling the callback from LinkedIn
    router.get('/linkedin/callback', async ({ ally }) => {
      const linkedin = ally.use('linkedinOpenidConnect')
    
      if (linkedin.accessDenied()) {
        return 'Access was denied'
      }
    
      if (linkedin.stateMisMatch()) {
        return 'State mismatch error'
      }
    
      if (linkedin.hasError()) {
        return linkedin.getError()
      }
    
      const user = await linkedin.user()
      return user
    })