Last active
February 4, 2025 14:27
-
-
Save andreacioni/eb5bad0bcca18cf4fb732c6d7e29e3e8 to your computer and use it in GitHub Desktop.
NestJS Authentication: Single Sign On with SAML 2.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Get('api/auth/sso/saml/login') | |
@UseGuards(SamlAuthGuard) | |
async samlLogin() { | |
//this route is handled by passport-saml | |
return; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Post('api/auth/sso/saml/ac') | |
@UseGuards(SamlAuthGuard) | |
async samlAssertionConsumer( | |
@Request() req: express.Request, | |
@Response() res: express.Response, | |
) { | |
//this routes gets executed on successful assertion from IdP | |
if (req.user) { | |
const user = req.user as User; | |
const jwt = this.authService.getTokenForUser(user); | |
this.userService.storeUser(user); | |
this, res.redirect('/?jwt=' + jwt); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@UseGuards(JwtAuthGuard) | |
@Get('api/profile') | |
getProfile(@Request() req: any) { | |
return req.user; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Controller() | |
export class AppController { | |
constructor( | |
private readonly authService: AuthService, | |
private readonly userService: UserService, | |
private readonly samlStrategy: SamlStrategy, //new dependency needed here | |
) {} | |
// [...] | |
@Get('api/auth/sso/saml/metadata') | |
async getSpMetadata(@Response() res: express.Response) { | |
const ret = this.samlStrategy.generateServiceProviderMetadata(null, null); | |
res.type('application/xml'); | |
res.send(ret); | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Get() | |
async homepage(@Response() res: express.Response) { | |
res.sendFile(resolve('web/index.html')); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@nestjs/common'; | |
import { JwtService } from '@nestjs/jwt'; | |
import { User } from '../model/user'; | |
@Injectable() | |
export class AuthService { | |
constructor(private jwtService: JwtService) {} | |
getTokenForUser(user: User) { | |
const payload = { | |
sub: user.username, | |
iss: user.issuer, | |
}; | |
return this.jwtService.sign(payload); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>Home</title> | |
<script> | |
const goToLoginPage = () => { | |
document.getElementById("user-profile-div").style = 'display: none;' | |
document.getElementById("login-div").style = '' | |
} | |
const goToProfilePage = () => { | |
document.getElementById("user-profile-div").style = '' | |
document.getElementById("login-div").style = 'display: none;' | |
} | |
const loadProfile =async () => { | |
const params = (new URL(document.location)).searchParams; | |
let jwt; | |
if(params.has("jwt")) { | |
jwt = params.get("jwt"); | |
localStorage.setItem('jwt', jwt); | |
window.location.href = '/' | |
} else { | |
jwt = localStorage.getItem('jwt') | |
} | |
const options = jwt ? { | |
headers: {'Authorization': 'Bearer ' + jwt} | |
} : undefined; | |
const res = await fetch("/api/profile", options); | |
if(!res.ok) { | |
localStorage.removeItem('jwt'); | |
goToLoginPage() | |
return | |
} else { | |
const json = await res.json() | |
goToProfilePage() | |
document.getElementById("userid").innerHTML = json.username; | |
document.getElementById("phone").innerHTML = json.phone; | |
document.getElementById("email").innerHTML = json.email; | |
} | |
} | |
const logout = () => { | |
localStorage.removeItem('jwt'); | |
goToLoginPage() | |
} | |
</script> | |
</head> | |
<body onload="loadProfile()"> | |
<div id="login-div"> | |
<p>User not logged in</p> | |
<p><a id="login" href="/api/auth/sso/saml/login">Log In</a></p> | |
</div> | |
<div style="display: none;" id="user-profile-div"> | |
<p>Hello <span id="userid"></span>!</p> | |
<ul> | |
<li>Email: <span id="email"></span></li> | |
<li>Phone: <span id="phone"></span></li> | |
</ul> | |
<p><a id="logout" href="#" onclick="logout()">Log Out</a></p> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@nestjs/common'; | |
import { AuthGuard } from '@nestjs/passport'; | |
@Injectable() | |
export class JwtAuthGuard extends AuthGuard('jwt') {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ExtractJwt, Strategy } from 'passport-jwt'; | |
import { PassportStrategy } from '@nestjs/passport'; | |
import { ForbiddenException, Injectable } from '@nestjs/common'; | |
import { jwtConstants } from './constants'; | |
import { UserService } from 'src/user/user.service'; | |
import { User } from '../model/user'; | |
@Injectable() | |
export class JwtStrategy extends PassportStrategy(Strategy) { | |
constructor(private readonly userService: UserService) { | |
super({ | |
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), | |
ignoreExpiration: false, | |
secretOrKey: jwtConstants.secret, | |
}); | |
} | |
async validate(payload: any) { | |
const user: User | undefined = this.userService.retrieveUser(payload.sub); | |
if (user) { | |
return user; | |
} | |
throw new ForbiddenException('user was not found'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Profile = { | |
issuer?: string; | |
sessionIndex?: string; | |
nameID?: string; | |
nameIDFormat?: string; | |
nameQualifier?: string; | |
spNameQualifier?: string; | |
mail?: string; // InCommon Attribute urn:oid:0.9.2342.19200300.100.1.3 | |
email?: string; // `mail` if not present in the assertion | |
getAssertionXml(): string; // get the raw assertion XML | |
getAssertion(): object; // get the assertion XML parsed as a JavaScript object | |
getSamlResponseXml(): string; // get the raw SAML response XML | |
ID?: string; | |
} & { | |
[attributeName: string]: unknown; // arbitrary `AttributeValue`s | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@nestjs/common'; | |
import { AuthGuard } from '@nestjs/passport'; | |
@Injectable() | |
export class SamlAuthGuard extends AuthGuard('saml') {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PassportStrategy } from '@nestjs/passport'; | |
import { ForbiddenException, Injectable } from '@nestjs/common'; | |
import { Strategy, Profile } from 'passport-saml'; | |
import { User } from '../model/user'; | |
@Injectable() | |
export class SamlStrategy extends PassportStrategy(Strategy) { | |
constructor() { | |
super({ | |
issuer: 'saml2-nest-poc', | |
callbackUrl: 'http://localhost:3000/api/auth/sso/saml/ac', | |
cert: 'MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOEjj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1klbN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF/cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8nspXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0GA1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVzdC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsFAAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHTTNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nblD1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcUZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==', | |
entryPoint: 'https://samltest.id/idp/profile/SAML2/Redirect/SSO', | |
wantAssertionsSigned: true, | |
}); | |
} | |
async validate(profile: Profile) { | |
try { | |
const user: User = { | |
username: profile['urn:oid:0.9.2342.19200300.100.1.1'] as string, | |
email: profile.mail as string, | |
issuer: profile.issuer as string, | |
phone: profile['urn:oid:2.5.4.20'] as string, | |
}; | |
return user; | |
} catch (e) { | |
throw new ForbiddenException('invalid user attributes'); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@nestjs/common'; | |
import { User } from '../model/user'; | |
@Injectable() | |
export class UserService { | |
private _store: Map<string, User>; | |
constructor() { | |
this._store = new Map<string, User>(); | |
} | |
storeUser(user: User): void { | |
this._store.set(user.username, user); | |
} | |
retrieveUser(id: string): User | undefined { | |
return this._store.get(id); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment