Collection of tips / guides for authentication on different platforms targeting University of Waterloo.
Info |
---|
NOTE: As of 2024, the recommended mechanism for authentication is DUO OIDC. This requires IST to grant access tokens in order to function. |
General Tips
Your domain will need valid HTTPS
Need a callback URL (usually handled by one of the solutions below).
Need to contact IST for a client key via a ticket
Combining authentication with Grouper defined NEXUS groups can be a robust solution
OIDC - Apache (mod_auth_openidc)
Install Apache module (debian/ubuntu)
Code Block |
---|
apt install -y libapache2-mod-auth-openidc
# enable module
a2enmod auth_openidc |
Configure the module and a basic VirtualHost
...
Quick Aside: How OIDC Auth kinda works
For those new to the technology, the basic premise is as follows:
Your website sends a https post to the OIDC authentication portal (this will navigate your user away from your website. Part of the request will be your callback url
Some stuff happens on the other site – usually the user will log in and then do some 2FA stuff – you don’t need to worry about it, as we trust the OIDC portal!
The portal will redirect to your callback url with an auth token – you can store this info and use parts of it to refresh itself. Honestly this bit gets a bit hairy, so if possible you should probably just use a library.
It’s possible to get AD group information sent through the token information, which can be very useful to separate roles on your website via Grouper.
Apache (mod_auth_openidc)
You can set up OIDC directly on your Apache / httpd server.
Install Apache module (debian/ubuntu)
Code Block |
---|
apt install -y libapache2-mod-auth-openidc
# enable module
a2enmod auth_openidc |
Configure the module and a basic VirtualHost
Code Block |
---|
# For advanced options see: https://github.com/OpenIDC/mod_auth_openidc
<IfModule mod_auth_openidc.c>
# afterIf successfullyou loginhave redirectan theingress userproxy tolike whereCaddy theyyou'll wantedneed tothe gofollowing
# this path needs to openid-connect protected on your host
OIDCRedirectURI respect X-Forwarded-* headers passed down from proxy
OIDCXForwardedHeaders X-Forwarded-Proto X-Forwarded-Host
# this personal secret is created by you and never shared!
https://myhost.fast.uwaterloo.ca/secure/redirect_uriOIDCCryptoPassphrase XXXXXXXXXXX_PERSONAL_SECRET_XXXXXXXXXXX
# OIDCProviderMetadataURLafter https://sso-4ccc589b.sso.duosecurity.com/oidc/XXXXXXXXXXXXXXXXXXXX/.well-known/openid-configuration
OIDCClientID successfull login redirect the user to where they wanted to go
# this path needs XXXXXXXXXXXXXXXXXXXXto openid-connect protected on your OIDCClientSecrethost
OIDCRedirectURI XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # NOTE: you can find scopes in the the OIDCProviderMetadataURL
OIDCScope "openid profile email"
OIDCRemoteUserClaim userhttps://myhost.fast.uwaterloo.ca/secure/redirect_uri
OIDCProviderMetadataURL https://sso-4ccc589b.sso.duosecurity.com/oidc/XXXXXXXXXXXXXXXXXXXX/.well-known/openid-configuration
OIDCClientID # use HTTP_ prefix so env varsXXXXXXXXXXXXXXXXXXXX
are more trusted in CGI.OIDCClientSecret "see: suexec safe_env_lst" OIDCClaimPrefix HTTP_OIDC_CLAIM_ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# WARNNOTE: defaultyou can delimiterfind is comma. AD groups can contain commas! (":" is safer)
OIDCClaimDelimiter ":"
</IfModule>scopes in the the OIDCProviderMetadataURL
OIDCScope "openid profile email"
OIDCRemoteUserClaim user
# Assuminguse handlingHTTP_ httpsprefix onso anenv ingressvars serverare likemore Caddy.trusted #in Also port 80 is firewalled to only talk to your ingress server.
<VirtualHost _default_:80>
ServerName myhost.fast.uwaterloo.ca
ServerAlias myhost-stage.fast.uwaterloo.ca
CGI. "see: suexec safe_env_lst"
OIDCClaimPrefix HTTP_OIDC_CLAIM_
# WARN: default delimiter is comma. AD groups can contain commas! (":" is safer)
OIDCClaimDelimiter ":"
</IfModule>
# NOTE:Assuming thishandling ishttps neededon foran theingress OIDCRedirectURIserver callbacklike Caddy.
# Also port <Location80 /secure>is firewalled to only talk to your ingress AuthType openid-connectserver.
<VirtualHost _default_:80>
ServerName myhost.fast.uwaterloo.ca
RequireServerAlias valid-usermyhost-stage.fast.uwaterloo.ca
</Location>
# example: only allow IdM-HR-staff usersNOTE: this is needed for the OIDCRedirectURI callback
<Location /staff>secure>
AuthType openid-connect
Require valid-user
claim group:IdM </Location>
# example: only allow IdM-HR-staff users
<Location /staff>
AuthType openid-connect
Require claim group:IdM-HR-staff
</Location>
# example: require user to be in two groups
<Location /staff-admin>
AuthType openid-connect
# each require is an OR. If you want AND, use RequireAll:
<RequireAll>
Require claim group:IdM-HR-staff
Require claim group:myhost-admin
</RequireAll>
# TODO: this might also work
# Require claim "group:myhost-admin group:IdM-HR-staff"
</Location>
</VirtualHost>
</Location>
</VirtualHost> |
Django - django-oidc-auth
django-oidc-auth is a library maintained by Mirko Vucicevich , Ryan Goggin and Steve Weber for simple OIDC auth via Django. It requires Django >= 3 and python >=3.9 (as of Feb 2024)
For the simplest configuration follow the instructions in the provided README.md, as the software has been designed and tested with campus OIDC configurations.
Expand |
---|
title | Active Directory (ADFS) [Old / Deprecated auth] |
---|
|
ADFS - Apache (Mellon) Create self signed key cert pair. Create metadata file and get current FederationMetadata.xml. |
...
ADFS - NGINX
REMOTE_USER using basic auth is supported. However IIRC for ADFS auth is not simple and requires more tricky to install extras. Personally Apache has improved over the past years and is again competitive with Nginx. Also Apache does not charge money for enterprise features like load balanced health checked nodes. Steve Weber says: just use Apache if you want to use ADFS + REOMTE_USER.
ADFS - Caddy
Still some WIP. Waiting on some upstream support of OIDC
Code Block |
---|
HOST=example.uwaterloo.ca
openssl genrsa -out key 2048
chmod 0600 key
openssl req -new -sha256 -x509 -days 10000 -subj "/C=CA/ST=Ontario/L=Waterloo/O=University\ of\ Waterloo/CN=$HOST" -key key -out signed.crt
# Simple MellonSPMetadataFile
<<EOF cat > MellonSPMetadataFile.xml
<EntityDescriptor entityID="https://$HOST" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>$(grep -v '^-----' "signed.crt")</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://$HOST/mellon/logout"/>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://$HOST/mellon/postResponse" index="0"/>
</SPSSODescriptor>
</EntityDescriptor>
EOF
# Pending on if its a test server or a dev server use adfs.uwaterloo.ca or adfstest.uwaterloo.ca
curl https://adfs.uwaterloo.ca/FederationMetadata/2007-06/FederationMetadata.xml > adfs_FederationMetadata.xml
curl https://adfstest.uwaterloo.ca/FederationMetadata/2007-06/FederationMetadata.xml > adfstest_FederationMetadata.xml |
Register your MellonSPMetadataFile.xml using the web form: https://uwaterloo.ca/request-tracking-system/adfs-request Perhaps set these claims: Code Block |
---|
Group
emailaddress
surname
givenname
samaccountname
UPN |
Install apache and mellon module Code Block |
---|
command -v yum && yum install -y mod_auth_mellon
command -v apt && apt install -y libapache2-mod-auth-mellon
# enable the mellon module
a2enmod mellon |
For this example you can copy your certs to the following locations, note you might want to review file mode and privileges. Code Block |
---|
cp key /etc/apache_mellon/key
cp MellonSPMetadataFile.xml /etc/apache_mellon/MellonSPMetadataFile.xml
cp adfs_FederationMetadata.xml /etc/apache_mellon/adfs_MellonIdPMetadataFile.xml
cp adfstest_FederationMetadata.xml /etc/apache_mellon/adfstest_MellonIdPMetadataFile.xml |
Apache config example using proxypass. Code Block |
---|
## If you want to send logs system journal
ErrorLog ${APACHE_LOG_DIR}/error.log
ErrorLog "|/usr/bin/systemd-cat --identifier=apache_error --priority=warning"
CustomLog ${APACHE_LOG_DIR}/access.log combined
LogFormat "%h %l %u \"%r\" %>s %O" systemd
CustomLog "|/usr/bin/systemd-cat --identifier=apache_access" systemd
## redirect http to https
<VirtualHost _default_:80>
ServerName example.uwaterloo.ca
Redirect permanent / https://{{vars.server_name}}/
</VirtualHost>
<VirtualHost _default_:443>
ServerName example.uwaterloo.ca
DocumentRoot /var/www/html
SSLEngine on
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
SSLProtocol -All +TLSv1.2
SSLCertificateFile {{vars.SSLCertificateFile}}
SSLCertificateKeyFile {{vars.SSLCertificateKeyFile}}
SSLCACertificateFile {{vars.SSLCACertificateFile}}
# optional logout url
<Location /adfs_logout>
Redirect /adfs_logout https://adfstest.uwaterloo.ca/adfs/ls/?wa=wsignout1.0&wreply=https://optional_adfs_logout_redirect
#PRODUCTION: Redirect /adfs_logout https://adfs.uwaterloo.ca/adfs/ls/?wa=wsignout1.0&wreply=https://optional_adfs_logout_redirect
</Location>
<Location />
# (optional) Redirect none VPN users to vpncheck page.
RewriteEngine On
RewriteCond expr "!(-R '127.0.0.0/8' || -R '10.0.0.0/8' || -R '172.16.0.0/12' || -R '192.168.0.0/16')"
RewriteCond expr "!(-R '129.97.0.0/16')"
RewriteCond expr "!(-R '47.252.27.26/32')"
RewriteRule ^(.*) https://checkvpn.uwaterloo.ca/?callback=https://{{vars.server_name}}%{REQUEST_URI} [R]
## More Configuration options are documented at:
## https://github.com/Uninett/mod_auth_mellon
Require valid-user
AuthType "Mellon"
MellonEnable "auth"
MellonSecureCookie On
MellonMergeEnvVars On ":"
MellonSetEnvNoPrefix HTTP_ADFS_EMAILADDRESS emailaddress
MellonSetEnvNoPrefix HTTP_ADFS_GIVENNAME givenname
MellonSetEnvNoPrefix HTTP_ADFS_SURNAME http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname
MellonSetEnvNoPrefix HTTP_ADFS_SAMACCOUNTNAME samaccountname
MellonSetEnvNoPrefix HTTP_ADFS_GROUP http://schemas.xmlsoap.org/claims/Group
## Example: Limit access to multiple groups
#MellonCond HTTP_ADFS_GROUP Math_G_Org_MFCF_Staff_Tech [MAP,OR]
#MellonCond HTTP_ADFS_GROUP Math_G_Org_MFCF_Staff_Woot [MAP,OR]
#MellonCond HTTP_ADFS_GROUP Math_G_Org_MFCF_Staff_Admin [MAP]
## Limit access to one group
MellonCond HTTP_ADFS_GROUP Math_G_Org_MFCF_Staff_Admin [MAP]
# session lifetime 1d; Actual lifetime may be shorter, depending on IdP settings
MellonSessionLength 86400
MellonSPPrivateKeyFile /etc/apache_mellon/key
MellonSPMetadataFile /etc/apache_mellon/MellonSPMetadataFile.xml
MellonIdPMetadataFile /etc/apache_mellon/adfstest_MellonIdPMetadataFile.xml
#PRODUCTION: MellonIdPMetadataFile /etc/apache_mellon/adfs_MellonIdPMetadataFile.xml
MellonRedirectDomains *
# (optional) Add headers with the claims to pass through the ProxyPass
# These claims can then be used for example in a PHP web service running at 127.0.0.1:8080
# NOTE: user provided headers would be overwritten http://www.ietf.org/rfc/rfc2616.txt
RequestHeader set HTTP_ADFS_EMAILADDRESS %{HTTP_ADFS_EMAILADDRESS}e
RequestHeader set HTTP_ADFS_GIVENNAME %{HTTP_ADFS_GIVENNAME}e
RequestHeader set HTTP_ADFS_SURNAME %{HTTP_ADFS_SURNAME}e
RequestHeader set HTTP_ADFS_SAMACCOUNTNAME %{HTTP_ADFS_SAMACCOUNTNAME}e
RequestHeader set HTTP_ADFS_GROUP %{HTTP_ADFS_GROUP}e
# if you want to pass the header to the clients browser for viewing
#DEBUG Header set HTTP_ADFS_GROUP %{HTTP_ADFS_GROUP}e
ProxyPass http://127.0.0.1:8080
</Location>
</VirtualHost> |
|
...
Code Block |
---|
MellonEnable auth
Require valid-user
AuthType Mellon
MellonCond ADFS_GROUP fast-admin [MAP]
#DEBUG: to view group header in browser
#Header set HTTP_ADFS_GROUP: %{HTTP_ADFS_GROUP}e |
example .htaccess Code Block |
---|
MellonEnable auth
Require valid-user
AuthType Mellon
MellonCond ADFS_GROUP fast-admin [MAP]
#DEBUG: to view group header in browser
#Header set HTTP_ADFS_GROUP: %{HTTP_ADFS_GROUP}e |
JavaScript (server-side)There's a million ways to go about this, easiest I (Mirko) have found so far is with Node + ExpressJS + PassportJS. |
...
title | Expand Express/Passport Details |
---|
The following configuration works with these installs: npm install expressjs@4.18 express-session@1.17 passport@0.6 passport-azure-ad@4.2 cookie-parser@1.4 I'm also using the dotenv package to add some variables from a .env file to process.env note the example is using node with .mjs files to enable module import / export index.mjs Code Block |
---|
| import express from 'express'
import session from 'express-session'
import passport from 'passport'
import cookieParser from 'cookie-parser'
import { OIDCStrategy } from 'passport-azure-ad'
import dotenv from 'dotenv'
dotenv.config()
const app = express()
app.use(session({
secret: process.env.SECRET_KEY||"PROVIDE_SECRET",
resave: false,
cookie: {maxAge: 7*24*60*60*1000} // Optionally add secure: true if https,
saveUninitialized: true
}))
app.use(cookieParser())
app.use(passport.initialize())
app.use(passport.session())
passport.use(new OIDCStrategy({
identityMetadata: `${process.env.ADFS_SERVER}/adfs/.well-known/openid-configuration`,
clientID: process.env.ADFS_CLIENT_ID,
responseType: 'id_token',
responseMode: 'form_post',
redirectUrl: `${process.env.HOSTNAME}/oauth2/callback`,
passReqToCallback: true,
//loggingLevel: 'info',
//scope: ['winaccountname'],
useCookieInsteadOfSession: true,
cookieEncryptionKeys: [ { key: '12345678901234567890123456789012', 'iv': '123456789012' }], // IDK what this does lol
},
function(req, iss, sub, profile, accessToken, refreshToken, done) {
// You'll probably want to do some work here
// You can pull out whatever you want from the profile here
let username = profile._json.winaccountname
if (!username) {
return done(new Error("No username found"), null);
}
return done(null, {username: username})
}
));
passport.serializeUser(function(user, done) {
// You can do some stuff here
done(null, user);
});
passport.deserializeUser(function(user, done) {
// Also here
done(null, user);
});
function regenerateSessionAfterAuthentication(req, res, next) {
let passportInstance = req.session.passport;
return req.session.regenerate(function (err){
if (err) {
return next(err);
}
req.session.passport = passportInstance;
return req.session.save(next);
})
}
function restrict(check){
// You can pass a check function here to add additional stuff!
return function (req, res, next) {
let cfn = check || function(){ return true }
if (req.user && cfn(req, res)) {
next();
} else {
// Do whatever you want here -- I'm returing json
res.status(403).json({error: 'not logged in, access denied!'})
}
}
}
app.get('/oauth2/login',
passport.authenticate('azuread-openidconnect', {
prompt: 'login'
})
)
app.post('/oauth2/callback',
passport.authenticate('azuread-openidconnect', { failureRedirect: process.env.SERVER_LOGIN_REDIRECT, prompt: 'login'}),
regenerateSessionAfterAuthentication,
function (req, res) {
// Successful authentication, redirect home.
res.redirect(process.env.SERVER_LOGIN_REDIRECT);
}
)
app.post('/oauth/logout/', (req, res, next)=>{
// Logout is broken with azure-ad, so we just do it manually!
if (req.session.passport) {
delete req.session.passport.user;
}
let prevSession = req.session;
req.session.save(function(err) {
if (err) {
return next(err)
}
// regenerate the session, which is good practice to help
// guard against forms of session fixation
req.session.regenerate(function(err) {
if (err) {
return next(err);
}
// You may want to redirect somewhere here!!
return res.status(200).json({'status': 'ok'})
});
});
})
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/restricted', restrict(), (req, res) => {
res.send(`Hello ${req.user.username}`)
})
// And run!
app.listen(process.env.PORT||3000, process.env.HOST||'localhost', ()=>{
console.log("Server up!")
}) |
In hindsight this isn't very easy at all. If you need help contact Mirko PHPI know at least one of you guys has this set up! Python - Django (django-auth-adfs)Django supports REMOTE_USER out of the box, so if you've already got that set up you're good to go! Alternatively Django has a popular package django-auth-adfs for oauth2 SSO. For the SSO solutions once you have your client key from IST follow the guide on the git repo to set up your LOGIN_URL, urls.py, and INSTALLED_APPS, then set the following in your settings.py: Code Block |
---|
| AUTH_ADFS = {
'SERVER': 'adfs.uwaterloo.ca', #adftest.uwaterloo.ca for testing server
'CLIENT_ID': 'your-key',
'RELYING_PARTY_ID': 'your-key',
'AUDIENCE': 'microsoft:identityserver:your-key',
'CLAIM_MAPPING': {
'first_name': 'given_name',
'last_name': 'family_name',
'email': 'email',
},
'USERNAME_CLAIM': 'winaccountname',
'MIRROR_GROUPS': False, # True if you want to overwrite django's groups with ADFS groups. It imports MANY groups
} |
If you've got it all set up you should be able to log in with ADFS, and Django user accounts will be created as normal. Former user (Deleted) supports a fork of the adfs-package with duo / SAML all set up (ryan pls fill out) |
Python - Flask
If anybody's done this please help out by filling this section!
Python - FastAPI
FastAPI is fairly barebones, but the following setup
Expand |
---|
title | View FastAPI Detailed Instructions |
---|
|
? did someone forget to include documents? |
.NET FrameworkWhen making your ADFS request ignore any instructions posted by IST; say you want it for .NET, you don't need to provide any metadata file. ' Full Framework/OWIN / somewhere in your app Startup Code Block |
---|
| using Microsoft.Owin.Security.WsFederation;
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = "your realm/app id you gave them, usually make it the root app URL",
MetadataAddress = "the ADFS metadata dev or production IST publishes on ADFS page"
}); |
Anywhere after: Code Block |
---|
| var claimsIdent = (ClaimsIdentity)User.Identity; |
|
...
.NET Core/5+
Todo.