Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Social auth] Easy refresh token management #917

Open
LoicPoullain opened this issue Apr 27, 2021 · 1 comment
Open

[Social auth] Easy refresh token management #917

LoicPoullain opened this issue Apr 27, 2021 · 1 comment

Comments

@LoicPoullain
Copy link
Member

Issue

Currently, Foal allows users to be authenticated through social providers. Through social services, the application can authenticate users with Google, for example, and in return get user information as well as access and refresh tokens to communicate with the Google API.

Issue 1
However, there is no "easy" way to store securely the refresh token for later communication with the API. We can store the refresh token in the session using ctx.session.set('refreshToken', tokens.refresh_token) but then the token is saved in plain text in the database. This can cause security problems if the database is corrupted. For this reason, Foal should provide an easy way to encrypt and decrypt refresh tokens.

Issue 2
Once the refresh token is retrieved, in a later request for example, we still need to take a look at the Google API (or other providers API) to see how to get a new access token. This task could simply be handled by the framework.

Possible solution

Add three new methods encryptRefreshToken, decryptRefreshToken and refreshAndGetAccessToken to each social service.

export class AuthController {
  @dependency
  google: GoogleProvider;

  @dependency
  store: Store;

  @Get('/signin/google')
  redirectToGoogle() {
    return this.google.redirect({
      scopes: [
        'email', 'openid', 'profile',
        // Request access to view the user's YouTube account.
        'https://www.googleapis.com/auth/youtube.readonly'
      ]
    });
  }

  @Get('/signin/google/callback')
  @UseSessions({
    cookie: true,
    csrf: false,
  })
  async handleGoogleRedirection(ctx: Context) {
    const { userInfo } = await this.google.getUserInfo(ctx);

    if (!userInfo.email) {
      throw new Error('Google should have returned an email address.');
    }

    let user = await User.findOne({ email: userInfo.email });

    if (!user) {
      // If the user has not already signed up, then add them to the database.
      user = new User();
      user.email = userInfo.email;
      await user.save();
    }

    ctx.session.setUser(user);
    ctx.session.set('refreshToken', this.google.encryptRefreshToken(tokens.refresh_token))

    return new HttpResponseRedirect('/');
  }

}
export class ApiController {
  @dependency
  google: GoogleProvider;

  @Get('/youtube-activities')
  @UseSessions({ cookie: true })
  async getYouTubeActivities(ctx: Context) {
    const encryptedRefreshToken = ctx.session.get<string|undefined>('refreshToken');
    if (!encryptedRefreshToken) {
      throw new Error('Refresh token not found');
    }

    const refreshToken = this.google.decryptRefreshToken(encryptedRefreshToken);

    const accessToken = await this.google.refreshAndGetAccessToken(refreshToken);

    const response = await fetch('https://www.googleapis.com/youtube/v3/activities', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    })

    const body = await response.json();

    if (!response.ok) {
      throw new Error(body);
    }

    return new HttpResponseOK(body);
  }

}
@LoicPoullain LoicPoullain added this to Backlog in Issue tracking via automation Apr 27, 2021
@LoicPoullain LoicPoullain moved this from Backlog to To Do in Issue tracking Apr 27, 2021
@LeonardoSalvucci
Copy link
Contributor

LeonardoSalvucci commented Oct 11, 2022

@LoicPoullain Hi! I remember that i've made two methods for encrypt and decrypt a string using a secret from config in AbstractProvider. This could be used for solving issue 1. 👍

/**
   * This function is for encrypt a string using aes-256 and codeVerifierSecret.
   * Notice that init vector base64-encoded is concatenated at start of encrypted message.
   * We'll need init vector to decrypt message.
   * Init vector is 16 bytes length and it base64-encoded is 24 bytes length.
   *
   * @param {string} message - String to encrypt
   */
  private encryptString(message: string): string {

    const hashedSecret = this.getCodeVerifierSecretBuffer();

    // Initiate iv with random bytes
    const initVector = crypto.randomBytes(16);

    // Create cipher
    const cipher = crypto.createCipheriv(this.cryptAlgorithm, hashedSecret, initVector);

    // Encrypt data, concat final
    const data = cipher.update(Buffer.from(message));
    const encryptedMessage = Buffer.concat([data, cipher.final()])

    return `${initVector.toString('base64')}${encryptedMessage.toString('base64')}`
  }

  /**
   * This function is for decrypt a string using aes-256 and codeVerifierSecret
   * encryptedMessage is {iv}{encrypted data}
   *
   * @param {string} encryptedMessage - String to decrypt
   */
    private decryptString(encryptedMessage: string): string {
      const hashedSecret = this.getCodeVerifierSecretBuffer();

      // Get init vector back from encryptedMessage
      const initVector: Buffer = Buffer.from(encryptedMessage.substring(0,24), 'base64'); // original iv is 16 bytes long, so base64 encoded is 24 bytes long
      const message: string = encryptedMessage.substring(24);

      // Create decipher
      const decipher = crypto.createDecipheriv(this.cryptAlgorithm, hashedSecret, initVector);

      // Decrypt data, concat final
      const data = decipher.update(Buffer.from(message, 'base64'));
      const decryptedMessage = Buffer.concat([data, decipher.final()])

      return decryptedMessage.toString()
    }

    private getCodeVerifierSecretBuffer(): Buffer {
      // Get secret from config file or throw an error if not defined
      const codeVerifierSecret = Config.getOrThrow(this.codeVerifierSecretPath, 'string');
      // We create a sha256 hash to ensure that key is 32 bytes long
      return crypto.createHash('sha256').update(codeVerifierSecret).digest();
    }
`
``

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Issue tracking
  
To Do
Development

No branches or pull requests

2 participants