# NestJS Harvard Key

This repository provides a way to connect NestJS applications to HarvardKey and
provide a rudimentary form of authorization (in the form of route guards) out
of the box

**NOTE: Version 2.x is meant for use with the new Okta implementation of
Harvard Key, and supports the OIDC protocol. Use the Version 1.x for the legacy
CAS protocol.**

## Installation

This package can be installed with npm:

```bash
npm install @seas-computing/nestjs-harvard-key
```

You will also need to have `express-session` installed in your project:

```bash
npm install express-session
```

## Configuration

### Session Required

Our HarvardKey setup requires session middleware to be in place. After signing in, the user data will be stored in the session and retrieved with the session ID cookie on subsequent requests.

Follow the [NestJS documentation](https://docs.nestjs.com/techniques/session) to set up session middleware in your application.

### Registering the Module

In the root module for your application, you will need to `import` the `HarvardKeyModule`, and provide several configuration values in the `useFactory` argument to `registerAsync`.


```typescript
import { HarvardKeyModule } from '@seas-computing/nestjs-harvard-key';

@Module({
  imports: [
    HarvardKeyModule.registerAsync({
      imports: [ConfigModule],
      injects: [ConfigService],
      useFactory: (config: ConfigService) => ({
        oidcCredentials: {
          clientID: config.OIDC_CLIENT_ID,
          clientSecret: config.OIDC_CLIENT_SECRET,
        },
        oidcBaseURL: config.OIDC_URL,
        serverBaseURL: config.SERVER_URL,
        clientBaseURL: config.CLIENT_URL,
      }),
    }),
  ],
})
```

For security reasons, it's recommended to provide any credentials through a `ConfigService` or environment variables, rather than hard-coding them.

## Usage

The `HarvardKeyModule` includes the following endpoints:

 - `POST /login`
 - `GET /validate`
 - `GET /logout`
 - `GET /user`

Sending a `POST` request to `/login` will redirect the user to the HarvardKey login page. After authenticating there, the user will be redirected to the `/validate` endpoint to complete the login process. After successfully validating, the user profile will be saved in the session as `req.session.user`.

### Recommended Flow

  1. Upon loading the app, the client should send a `GET` request to `/user` to check if the user is already logged in.
  2. If the user is not logged in, the client should send a `POST` request to `/login` to start the login process.
  3. After the user logs in at HarvardKey, they will be redirected back to the app, and the client should again send a `GET` request to `/user` to retrieve the logged-in user's profile.

### Guards

#### `Authentication` Guard

Within the application, you can use the `Authentication` guard to protect routes that require a user to be logged in. This will check for the presence of `req.session.user`, and if not found, will return a `403 Forbidden` response.

NOTE: The `Authentication` guard will not trigger the login flow.

```ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Authentication } from '@seas-computing/nestjs-harvard-key';

@Controller('protected')
@UseGuards(Authentication)
export class ProtectedController {
  @Get('/')
  getProtectedResource() {
    return { message: 'This is a protected resource' };
  }
}
```

#### `RequireGroup` Guard

The `RequireGroup` guard can be used to protect routes that require the user to be a member of a specific HarvardKey group. You can provide one or more group names as arguments to the guard, and it will check if the logged-in user belongs to any of those groups. If not, it will return a `403 Forbidden` response.

The `RequireGroup` guard should be used in conjunction with the `Authentication` guard to ensure that the user is logged in before checking group membership.

```ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Authentication, RequireGroup} from '@seas-computing/nestjs-harvard-key';

@Controller('protected')
@UseGuards(Authentication)
export class ProtectedController {
  @Get('/')
  public getProtectedResource() {
    return { message: 'This is a protected resource' };
  }

  @Get('/admin')
  @UseGuards(RequireGroup('admin'))
  public getAdminResource() {
    return { message: 'This is an admin-only resource' };
  }
}
```

## Testing

This packages also provides utilities to help with testing routes that are protected by the `Authentication` guard, and meant to be used in conjunction with the [NestJS testing utilities](https://docs.nestjs.com/fundamentals/testing).

We use the [Sinon library]() to stub out the `login()` method of our `TestingStrategy`, then replace the `HarvardKeyStrategy` provider and `Authentication` guard with our `TestingStrategy` and `TestingGuard` implementations, respectively.

```ts
import {
  HarvardKeyModule,
  TestingGuard,
  TestingStrategy,
  HarvardKeyGuard,
  HarvardKeyStrategy,
} from '@seas-computing/nestjs-harvard-key';
import { TestingModule, Test } from '@nestjs/testing';
import session, { MemoryStore } from 'express-session';
import { stub, SinonStub } from 'sinon';

const OIDC_CLIENT_ID = 'test-client-id';
const OIDC_CLIENT_SECRET = 'test-client-secret';
const CLIENT_URL = 'https://client.example.edu';
const OIDC_URL = 'https://login.example.edu';
const SERVER_URL = 'https://server.example.edu/app';

describe('ProtectedController', function () {
  let modeuleRef: TestingModule;
  let authStub: SinonStub;
  let api: ReturnType<typeof supertest>;

  beforeEach(async function {
    authStub = stub(TestingStrategy.prototype, 'login');

    moduleRef = await Test.createTestingModule({
      imports: [
        HarvardKeyModule.registerAsync({
          useFactory: () => ({
            oidcCredentials: {
              clientID: OIDC_CLIENT_ID,
              clientSecret: OIDC_CLIENT_SECRET,
            },
            oidcBaseURL: OIDC_URL,
            serverBaseURL: SERVER_URL,
            clientBaseURL: CLIENT_URL,
          }),
        }),
      ],
      providers: [
        TestingStrategy,
      ],
      controllers: [
        ProtectedController
      ],
    })
    // We need to override the default strategy and guard with our mock
    // implementations. This will replace the HarvardKey authentication flow,
    // so our tests will need to call /login and /validate to get the session
    // set up correctly.
      .overrideProvider(HarvardKeyStrategy)
      .useClass(TestingStrategy)
      .overrideGuard(Authentication)
      .useClass(TestingGuard)
      .compile();

    const nestApp = await moduleRef
      .createNestApplication()
    // Setting up a fake session with the default MemoryStore to hold session data
      .use(session({
        secret: 'test',
        resave: true,
        saveUninitialized: true,
      }))
      .init();

    api = supertest.agent(nestApp.getHttpServer());
  });

  afterEach(async function {
    authStub.restore();
    await moduleRef.close();
  });

  describe('GET /protected', function() {
    it('should return 200 for authenticated user', async function() {
      // resolve a mock user to simulate an authenticated user
      authStub.resolves({
        id: 'user1',
        displayName: 'Test User',
        memberof: [],
      });
      const result = await api.get('/protected').withCredentials().expect(200);
    });
    it('should return 403 for unauthenticated user', async function() {
      // resolve null to simulate an unauthenticated user
      authStub.resolves(null);
      const result = await api.get('/protected').withCredentials.expect(401);
    });
  });
});
```
