Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 32 Next »

Collection of tips / guides for authentication on different platforms targeting University of Waterloo.

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

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)

apt install -y libapache2-mod-auth-openidc
# enable module
a2enmod auth_openidc

Configure the module and a basic VirtualHost

# For advanced options see: https://github.com/OpenIDC/mod_auth_openidc
<IfModule mod_auth_openidc.c>
    # If you have an ingress proxy like Caddy you'll need the following
    # 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!
    OIDCCryptoPassphrase XXXXXXXXXXX_PERSONAL_SECRET_XXXXXXXXXXX
    # after successfull login redirect the user to where they wanted to go
    # this path needs to openid-connect protected on your host
    OIDCRedirectURI          https://myhost.fast.uwaterloo.ca/secure/redirect_uri
    OIDCProviderMetadataURL  https://sso-4ccc589b.sso.duosecurity.com/oidc/XXXXXXXXXXXXXXXXXXXX/.well-known/openid-configuration
    OIDCClientID             XXXXXXXXXXXXXXXXXXXX
    OIDCClientSecret         XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    # NOTE: you can find scopes in the the OIDCProviderMetadataURL
    OIDCScope "openid profile email"
    OIDCRemoteUserClaim user
    # use HTTP_ prefix so env vars are more trusted in CGI. "see: suexec safe_env_lst"
    OIDCClaimPrefix HTTP_OIDC_CLAIM_
    # WARN: default delimiter is comma. AD groups can contain commas! (":" is safer)
    OIDCClaimDelimiter ":"
</IfModule>

# Assuming handling https on an ingress server like Caddy.
# 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
    
    # NOTE: this is needed for the OIDCRedirectURI callback
    <Location /secure>
        AuthType openid-connect
        Require valid-user
    </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>

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.

 Active Directory (ADFS) [Old / Deprecated auth]

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 (server-side)

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;




  • No labels