jaredhanson / oauth2orize

OAuth 2.0 authorization server toolkit for Node.js.
https://www.oauth2orize.org?utm_source=github&utm_medium=referral&utm_campaign=oauth2orize
MIT License
3.47k stars 469 forks source link

Client is undefined during exchange part of Authorization Code grant #246

Closed anapeksha closed 2 months ago

anapeksha commented 2 months ago

I am integrating oauth2orize with Nestjs to create an OAuth server which supports only Authorization code grant type.

oauth.service.ts

import {
  Injectable,
  InternalServerErrorException,
  NotFoundException,
  UnauthorizedException,
} from "@nestjs/common";
import { Client, User } from "@prisma/client";
import oauth2orize, {
  ExchangeDoneFunction,
  IssueGrantCodeDoneFunction,
  OAuth2Server,
  exchange,
  grant,
} from "oauth2orize";
import { PrismaService } from "../prisma/prisma.service";

@Injectable()
export class OAuthService {
  private _server = oauth2orize.createServer<Client, User>();

  constructor(private readonly prismaService: PrismaService) {
    this._server.serializeClient((client, done) => {
      return done(null, client.id);
    });

    this._server.deserializeClient(async (id, done) => {
      try {
        const client = await this.prismaService.client.findUniqueOrThrow({
          where: { id },
        });
        done(null, client);
      } catch (err) {
        if (err.code === "P2025") {
          done(new UnauthorizedException("Client not found"));
        }
        done(new InternalServerErrorException(err));
      }
    });

    this._server.grant(
      grant.code(
        async (
          client,
          redirectUri,
          user,
          res,
          req,
          done: IssueGrantCodeDoneFunction
        ) => {
          try {
            const authorizationCode =
              await this.prismaService.authorizationCode.create({
                data: {
                  clientId: client.id,
                  userId: user ? user.id : client.userId,
                  redirectUri,
                  scope: req.scope,
                  state: req.state,
                  expiresAt: new Date(Date.now() + 10 * 60 * 1000),
                },
              });
            done(null, authorizationCode.code);
          } catch (err) {
            done(
              new InternalServerErrorException(
                "Failed to create authorization code"
              )
            );
          }
        }
      )
    );

    this._server.exchange(
      exchange.code(
        async (
          client,
          code,
          redirectUri,
          body,
          authInfo,
          done: ExchangeDoneFunction
        ) => {
          try {
            if (!client) {
              return done(new UnauthorizedException("Client undefined"));
            }
            const authCode =
              await this.prismaService.authorizationCode.findUnique({
                where: { code },
                include: { Client: true, User: true },
              });
            if (!authCode) {
              return done(new UnauthorizedException("Invalid Grant Code"));
            }

            if (authCode.clientId !== client.id) {
              return done(new UnauthorizedException("Client Id mismatch"));
            }

            if (authCode.redirectUri !== redirectUri) {
              return done(new UnauthorizedException("Redirect URI mismatch"));
            }

            const accessToken = await this.prismaService.accessToken.create({
              data: {
                clientId: client.id,
                userId: authCode.userId,
                scope: authCode.scope,
                expiresAt: new Date(Date.now() + 3600 * 1000),
              },
            });

            await this.prismaService.authorizationCode.delete({
              where: { id: authCode.id },
            });

            done(null, accessToken.token);
          } catch (err) {
            done(new InternalServerErrorException(err));
          }
        }
      )
    );
  }

  public getServer(): OAuth2Server<Client, User> {
    return this._server;
  }

  public async findClientById(clientId: string) {
    try {
      return await this.prismaService.client.findUniqueOrThrow({
        where: {
          id: clientId,
        },
      });
    } catch (err) {
      if (err.code === "P2025") {
        throw new NotFoundException("Client not found");
      }
      throw new InternalServerErrorException(err);
    }
  }

  public async findAccessToken(token: string) {
    try {
      return await this.prismaService.accessToken.findUniqueOrThrow({
        where: {
          token,
        },
      });
    } catch (err) {
      if (err.code === "P2025") {
        throw new UnauthorizedException("Token not found");
      }
      throw new InternalServerErrorException(err);
    }
  }
}

oauth.module.ts

import {
  MiddlewareConsumer,
  Module,
  NestModule,
  RequestMethod,
} from "@nestjs/common";
import { Client } from "@prisma/client";
import { ValidateDoneFunction } from "oauth2orize";
import { PrismaService } from "../prisma/prisma.service";
import { OAuthController } from "./oauth.controller";
import { OAuthService } from "./oauth.service";

@Module({
  controllers: [OAuthController],
  providers: [OAuthService, PrismaService],
})
export class OAuthModule implements NestModule {
  constructor(private oAuthService: OAuthService) {}
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(
        this.oAuthService
          .getServer()
          .authorization(
            async (
              clientId,
              redirectUri,
              done: ValidateDoneFunction<Client>
            ) => {
              try {
                const client = await this.oAuthService.findClientById(clientId);
                return done(null, client, redirectUri);
              } catch (err) {
                done(err);
              }
            }
          )
      )
      .forRoutes({ path: "oauth/authorize", method: RequestMethod.GET });

    consumer
      .apply(this.oAuthService.getServer().decision())
      .forRoutes({ path: "oauth/decision", method: RequestMethod.POST });

    consumer
      .apply(
        this.oAuthService.getServer().token(),
        this.oAuthService.getServer().errorHandler()
      )
      .forRoutes({
        path: "oauth/token",
        method: RequestMethod.POST,
      });
    return;
  }
}

authentication has been excluded to test the authorization flow.

I have logged the clients in multiple places, it exists everywhere. But, not in the exchange step.

anapeksha commented 2 months ago

My bad! Needed to attach the client to req.user before passing control to token().