Azure AD - How to create your own SAML-based application using new Azure Portal


With new Azure Portal (https://portal.azure.com/), Azure AD provides very flexible SAML-based configuration, but some folks ask me where to do that ?
In this post, I show you the answer for this question using some bit of SAML-based federation sample code of PHP and Node.js.

Note : For the settings using Azure Classic Portal (Azure Management Portal), see my previous posts "Azure AD Web SSO with PHP (Japanese)" and "Azure AD Web SSO with Node.js (Japanese)".

Settings with Azure Portal

First of all, I'll  show you how the SAML settings page is improved by new Azure Portal.

When you want to register your own SAML-based application, select "Azure Active Directory" in Azure Portal, click "Enterprise applications" menu, and push "add" button.
You can select a lot of pre-defined (registered) applications (like Salesforce, Google, etc), but you click "add your own" link on top of this page.

In the next screen, select "Deploying an existing application" drop-down and input your app name.

After you've added your application, select "Single sign-on" menu in your app settings page and select "SAML-based Sign-on" in "Mode" drop-down menu. (see the following screenshot)
By these steps, you can configure several SAML settings in this page.

First, you must specify your application identifier (which is used as entityID in SAML negotiation), and your app's reply url. (Here we set "mytestapp" as identifier. We use this identifier in the following custom federation applications.)
You can also specify the relay state in this section.

In the next "attributes" section, you can select which attributes should be used as the user identifier (which is returned as NameID by Azure AD in SAML negotiation), and you can also select the claims which should be returned.
When you were using Azure Classic Portal (https://manage.windowsazure.com/), you cannot specify this value and Azure AD always returned the original pairwise identifier as NameID. Some applications which need the e-mail format user principal name as NameID was used to have the trouble to federate Azure AD, but now we don't have these kind of troubles with new Azure Portal settings.

In the next "certificate" section, you can create the certificate and make the rollover certificate active. Here we create this certificate and make active for the following custom code.

Custom code by PHP (simpleSAMLphp)

Now let's start to create the code and federate with Azure AD.
First we use PHP, and here we use simpleSAMLphp for the SAML federation.

You first install IIS and PHP in your dev machine, and make sure that the following extensions are set in PHP.ini file.

extension=php_openssl.dll
extension=php_ldap.dll

Next you download simpleSAMLphp (see here), and publish {simplesamplephp install location}/www folder using IIS manager.

Remember that the page is redirected to https://{published simpleSAMLphp site}/module.php/saml/sp/saml2-acs.php/default-sp, when the user is successfully logged-in to Azure AD with SAML federation. Then you must set this url as "Reply URL" in your app settings in Azure Portal. (see the following screenshot)

Open {simplesamplephp location}\config\config.php and change "baseurlpath" to your previously published url. Moreover you must change "auth.adminpassword" to your favorite password. (The default password value is "123".)

<?php
$config = array (
  . . .

  'baseurlpath'           => 'simplesaml/',
  'certdir'               => 'cert/',
  'loggingdir'            => 'log/',
  'datadir'               => 'data/',
  . . .

  /**
   * This password must be kept secret, and modified from the default value 123.
   * This password will give access to the installation page of simpleSAMLphp with
   * metadata listing and diagnostics pages.
   * You can also put a hash here; run "bin/pwgen.php" to generate one.
   */
  'auth.adminpassword'    => 'test',
  'admin.protectindexpage'  => false,
  'admin.protectmetadata'    => false,
  . . .

Edit {simplesamplephp location}\config\authresources.php and make sure to change entityID with the previous application identifier.

$config = array(
  . . .

  'default-sp' => array(
    'saml:SP',

    'entityID' => 'mytestapp',

    'idp' => NULL,

    'discoURL' => NULL
  ),
  . . .

Next you copy the federation information in Azure Portal beforehand, and set this information using simpleSAMLphp UI.
First you must click "Configure {your app name}" in your app single sign-on settings page in Azure Portal.

In the configuration page, click "SAML XML Metadata" link (see the following screenshot), and the metadata file is downloaded in your local machine. Please copy the content (text) in the downloaded text file.
Note that this string content includes the digital signature by the certificate. For this reason, you shouldn't never change this text, even if it's space character.

Next you go to the simpleSAMLphp www site (in this example, https://localhost/simplesaml/index.php) using your web browser.
In the simpleSAMLphp settings page, click "Federation" tab and "Login as administrator" link. When the login screen is prompted, you enter "admin" as user id and password which you specified above.

After logged-in, click "XML to simpleSAMLphp metadata converter" link in the page (see the above screenshot), and the following metadata parser page is displayed.
Please paste your metadata which is previously copied into this textbox, and push "Parse" button. Then the converted metadata settings (which is written with PHP) is displayed in the bottom of this page. (See the following screenshot.)
Copy this PHP code, and paste it into the code of {simplesamplephp location}\metadata\saml20-idp-remote.php. (see the following code)

<?php
...

$metadata['https://sts.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/'] = array (
  'entityid' => 'https://sts.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/',
  'contacts' => 
  array (
  ),
  'metadata-set' => 'saml20-idp-remote',
  'SingleSignOnService' => 
  array (
    0 => 
    array (
      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
      'Location' => 'https://login.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/saml2',
    ),
    1 => 
    array (
      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
      'Location' => 'https://login.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/saml2',
    ),
  ),
  'SingleLogoutService' => 
  array (
    0 => 
    array (
      'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
      'Location' => 'https://login.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/saml2',
    ),
  ),
  'ArtifactResolutionService' => 
  array (
  ),
  'keys' => 
  array (
    0 => 
    array (
      'encryption' => false,
      'signing' => true,
      'type' => 'X509Certificate',
      'X509Certificate' => 'MIIC8DC...',
    ),
  ),
);

Note : On the contrary, if you want to set SAML federation SP (service provider) metadata (which includes the value of SingleLogoutService, etc) into Azure AD, you can get this XML from simpleSAMLphp and set it into Azure AD using the application manifest in Azure AD settings.

The settings of simpleSAMLphp has all done !

Let's create your own PHP (.php) code with simpleSAMLphp like the following code. This sample code is just showing all claims returned by Azure AD.

<?php
  require_once("../simplesamlphp-1.11.0/lib/_autoload.php");
  $as = new SimpleSAML_Auth_Simple('default-sp');
  $as->requireAuth();
  $attributes = $as->getAttributes();
?>
<div style="font-weight: bold;">Hello, PHP World</div>
<table border="1">
<?php  foreach ($attributes as $key => $value): ?>
  <tr>
    <td><?=$key;?></td>
    <td><?=$value[0];?></td>
  </tr>
<?php endforeach;?>
</table>

Let's see how it works.
If you access to this PHP page with your web browser, the page is redirected to the idp selector. In this page, please select the metadata of Azure AD, and push "Select" button.

Then the page is redirected into the Azure AD login (sign-in) page. Please input your login id and password.

When the login is succeeded, the returned claims are shown as follows in your custom PHP page.

Custom code by Node.js (express, passport)

When you use Node.js, the concept is the same as before. You can just use your favorite SAML library with your custom code, and configure the library with the registered Azure AD app settings.
Here we use the famous passport module with express framework in Node.js.

First you start to install express framework and express command.

npm install express -g
npm install -g express-generator

Create the project directory, and provision express project by the "express" command with the following commands. (The files and folders of template project are created, and all related packages are installed.)
After that, you can start and view the express project with your web browser. (Please run by "npm start", and access with your web browser.)

mkdir sample01
express -e sample01
cd sample01
npm install

Next, install passport and related modules with the following commands.

npm install express-session
npm install passport
npm install passport-saml

Open and edit app.js (the start-up js file for this express framework), and please add the following code (of bold font).
I explain about this code later.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var passport = require('passport');
var session = require('express-session');
var fs = require('fs');

var SamlStrategy = require('passport-saml').Strategy;
passport.serializeUser(function (user, done) {
  done(null, user);
});
passport.deserializeUser(function (user, done) {
  done(null, user);
});
passport.use(new SamlStrategy(
  {
    path: '/login/callback',
    entryPoint: 'https://login.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/saml2',
    issuer: 'mytestapp',
    cert: fs.readFileSync('MyTestApp.cer', 'utf-8'),
    signatureAlgorithm: 'sha256'
  },
  function(profile, done) {
    return done(null,
    {
      id: profile['nameID'],
      email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
      displayName: profile['http://schemas.microsoft.com/identity/claims/displayname'],
      firstName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'],
      lastName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname']
    });
  })
);

var index = require('./routes/index');
var users = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session(
  {
    resave: true,
    saveUninitialized: true,
    secret: 'this shit hits'
  }));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);

app.get('/login',
  passport.authenticate('saml', {
    successRedirect: '/',
    failureRedirect: '/login' })
  );
app.post('/login/callback',
  passport.authenticate('saml', {
    failureRedirect: '/',
    failureFlash: true }),
  function(req, res) {
    res.redirect('/');
  }
);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Here I explain about this sample code :

  • When SAML authentication and idp redirection is needed, the entryPoint url (here, https://login.windows.net/16d103a1-a264-4d36-9b52-51fa01ce5c2e/saml2) is used. Please copy this value from your app configuration page in Azure AD, and paste it. (see the following screenshot)
  • Please see the routing code, app.get('/login', ...);
    When the user goes to /login by the web browser, the SAML flow is proceeded and the user is redirected to the entryPoint url.
  • The path /login/callback is the reply url. When the authentication is succeeded in the identity provider (Azure AD), the results (SAML response) is returned to this url and the claims (here nameID, emailaddress, displayname, givenname, and surname) are parsed. (see return done(null, { id: ..., email: ..., displayName:..., ... }); in the above code.)
    After that, the page is redirected to /. (Please see the routing code, app.post('/login/callback', ...);)
    Thus please set this url as reply url in Azure AD app settings beforehand.
  • Please copy the X509 cert in Azure AD app settings or download the cert (see the following screenshot), and you set this cert as the passport-saml strategy.
    If you set this cert, the passport-saml module validates the incoming SAML response. (The passport-saml checks if the response is not altered by the malicious code.)

Finally let's get the returned claims and show these values in your page.
Here, we edit routes/index.js, and modify as follows. We're retrieving the user's displayName and email address, and passing to the view page.

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  if(req.isAuthenticated())
    res.render('index', { username: req.user.displayName, mail: req.user.email });
  else
    res.render('index', { username: null});
});

module.exports = router;

Edit views/index.ejs (which is the view page of the previous index.js), and modify as follows.

<!DOCTYPE html>
<html>
  <head>
    <title>SAML Test</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <% if (!username) { %>
    <h2>Not logged in...</h2>
    <% } else { %>
    <h2>Hello, <%= username %>.</h2>
    (your e-mail is <%=mail %>)
    <% } %>
  </body>
</html>

Your programming has finished !

Note : Your app must be hosted by https, and please configure to use https. (Here I don't describe this steps.)

Please start your app using the following command.

npm start

When you access to /login using your web browser, the page is redirected and the Azure AD sign-in page is displayed.

When you succeed your login, your display name and email are displayed in the top page (index.ejs) as follows.

 

If you're ISV folks, you can submit your own custom app (which is federated with Azure AD) to Azure AD gallery. Everyone can start and use your app (ISV app) federated with Azure AD with a few clicks !


Comments (2)

  1. Nate says:

    When I click "SAML XML Metadata" to download the IDP, I get a 401. The other links above work:

    https://main.iam.ad.ext.azure.com/api/ApplicationSso/%5Bobject id]/GetCertificateByThumbprint/[signing cert thumbprint] -- ok
    https://main.iam.ad.ext.azure.com/api/ApplicationSso/%5Bobject id]/GetMetadataByThumbprint/[signing cert thumbprint] -- fails, 401

    I can get a sort-of working IDP here:
    https://login.microsoftonline.com/%5Btenant id]/FederationMetadata/2007-06/FederationMetadata.xml

    ...but this does not include the Signing Certificate being used, so none of the Assertions are able to validate.

    What am I doing wrong? Why do I get a 401 trying to download the metadata?

    1. Sorry, but I don't figure out why 401 occurs.
      Is the Authorization header value assigned for calling https://main.iam.ad.ext.azure.com/api/ApplicationSso/... ? (You can see using the fiddler.)
      The JavaScript calls this url using XMLHttpRequest, but if it's blank, something in js isn't going well and it might output the errors in js console...

Skip to main content