Skip to content

Commit

Permalink
proof of concept for oidc against rea id and then using that to auth …
Browse files Browse the repository at this point in the history
…against backend
  • Loading branch information
ChristopherJMiller committed Dec 10, 2023
1 parent 42e4dcf commit e709ea0
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 12 deletions.
1 change: 0 additions & 1 deletion api/.env.development

This file was deleted.

5 changes: 5 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.1",
"@types/passport-jwt": "^3.0.13",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"generate-password": "^1.7.1",
"jwks-rsa": "^3.1.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"postgres": "^3.4.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
Expand Down
6 changes: 4 additions & 2 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UsersModule } from './users/users.module';

// 3rd Party Modules
const LIBRARY_IMPORTS = [
ConfigModule.forRoot({ cache: true, isGlobal: true, envFilePath: [".env.development", ".env"] }),
ConfigModule.forRoot({ cache: true, isGlobal: true, envFilePath: [".env", ".env.development"] }),
TypeOrmModule.forRoot(getDatabaseConfig()),
];

Expand All @@ -24,6 +24,8 @@ const FEATURE_IMPORTS = [
...FEATURE_IMPORTS,
],
controllers: [AppController],
providers: [AppService],
providers: [
AppService
],
})
export class AppModule {}
28 changes: 28 additions & 0 deletions api/src/users/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: 'https://id.realliance.net/application/o/profile/jwks/',
}),

jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: 'LNXdUuOZUue5HPlw8Vyglo83sIYndFaGUCIdQrSZ',
issuer: 'https://id.realliance.net/application/o/profile/',
algorithms: ['RS256'],
});
}

validate(payload: unknown): unknown {
return payload;
}
}
13 changes: 13 additions & 0 deletions api/src/users/user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class UserController {
constructor() {}

@Get('profile')
@UseGuards(AuthGuard('jwt'))
getProfile(): string {
return 'OK';
}
}
15 changes: 12 additions & 3 deletions api/src/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { UserController } from './user.controller';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';

@Module({
// forFeature is what triggers the entity to be loaded into the database management system as a persistant object type
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [],
imports: [
TypeOrmModule.forFeature([User]),
PassportModule.register({ defaultStrategy: 'jwt' })
],
providers: [
UsersService,
JwtStrategy
],
controllers: [UserController],
exports: [UsersService],
})
export class UsersModule {}
5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --open",
"dev": "vite --open --port 8080",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@types/js-cookie": "^3.0.6",
"autoprefixer": "^10.4.16",
"flowbite": "^2.2.0",
"flowbite-react": "^0.7.0",
"jose": "^5.1.3",
"js-cookie": "^3.0.5",
"oauth4webapi": "^2.4.0",
"postcss": "^8.4.32",
"react": "^18.2.0",
"react-cookie": "^6.1.1",
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Button } from "flowbite-react"
import { beginAuthFlow, onRedirect } from "./util/oauth"
import { useEffect } from "react"

function App() {
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);

if (urlParams.get('code') !== null) {
onRedirect()
}
}, []);

return (
<div className="container mx-auto py-2">
Nothing to see here yet.
<Button onClick={() => beginAuthFlow()}>Authenticate</Button>
</div>
)
}
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/util/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as oauth from 'oauth4webapi';
import Cookies from 'js-cookie';

const ISSUER = new URL('https://id.realliance.net/application/o/profile/');
const REDIRECT_URI = import.meta.env.PROD ? "https://profile.realliance.net" : "http://localhost:8080";

const as = await oauth
.discoveryRequest(ISSUER)
.then((response) => oauth.processDiscoveryResponse(ISSUER, response));

const client: oauth.Client = {
client_id: 'LNXdUuOZUue5HPlw8Vyglo83sIYndFaGUCIdQrSZ',
token_endpoint_auth_method: 'none',
}

export async function beginAuthFlow() {


if (as.code_challenge_methods_supported?.includes('S256') !== true) {
// This example assumes S256 PKCE support is signalled
// If it isn't supported, random `nonce` must be used for CSRF protection.
throw new Error("S256 PKCE not supported");
}

const code_verifier = oauth.generateRandomCodeVerifier()
const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier)
const code_challenge_method = 'S256'

Cookies.remove('codeVerifier');
Cookies.set('codeVerifier', code_verifier);

const authorizationUrl = new URL(as.authorization_endpoint!)
authorizationUrl.searchParams.set('client_id', client.client_id)
authorizationUrl.searchParams.set('code_challenge', code_challenge)
authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method)
authorizationUrl.searchParams.set('redirect_uri', REDIRECT_URI)
authorizationUrl.searchParams.set('response_type', 'code')
authorizationUrl.searchParams.set('scope', 'openid profile')

window.location.href = authorizationUrl.toString();
}

export async function onRedirect() {
const currentUrl = new URL(window.location.href);
const params = oauth.validateAuthResponse(as, client, currentUrl, oauth.skipStateCheck)
if (oauth.isOAuth2Error(params)) {
console.log('error', params)
throw new Error("Oauth2 Failed") // Handle OAuth 2.0 redirect error
}

const code_verifier = Cookies.get('codeVerifier') ?? "";
Cookies.remove('codeVerifier');

const response = await oauth.authorizationCodeGrantRequest(
as,
client,
params,
REDIRECT_URI,
code_verifier,
)

let challenges: oauth.WWWAuthenticateChallenge[] | undefined
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
for (const challenge of challenges) {
console.log('challenge', challenge)
}
throw new Error() // Handle www-authenticate challenges as needed
}

const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response)
if (oauth.isOAuth2Error(result)) {
console.log('error', result)
throw new Error() // Handle OAuth 2.0 response body error
}

console.log('result', result)
const claims = oauth.getValidatedIdTokenClaims(result)
console.log('ID Token Claims', claims)

const res = await fetch('http://localhost:3000/api/profile', {
headers: {
"Authorization": `Bearer ${result.access_token}`,
}
});

console.log(await res.text());
}
Loading

0 comments on commit e709ea0

Please sign in to comment.