Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

New projects should avoid using ADFS directly if possible, and use DUO OIDC instead. This documentation is here for reference purposes.

Expand
titleExpand ADFS Configuration guides

ADFS - Apache (Mellon)

Create self signed key cert pair. Create metadata file and get current FederationMetadata.xml.

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>

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 (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

Code Block
languagejs
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:

Code Block
languagepy
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

Code Block
languagec#
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
languagec#
var claimsIdent = (ClaimsIdentity)User.Identity;

...