ADFS - Apache (Mellon)
Create self signed key cert pair. Create metadata file and get current FederationMetadata.xml.
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:
Group
emailaddress
surname
givenname
samaccountname
UPN
Install apache and mellon module
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.
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.
## 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>
example .htaccess
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 (express / passport)
There's a million ways to go about this, easiest I (Mirko) have found so far is with Node + ExpressJS + PassportJS.
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
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
PHP
I 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:
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)
.NET Framework
When 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
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:
var claimsIdent = (ClaimsIdentity)User.Identity;