PHP: Implement Azure AD login to your site

A while ago I wrote some code to enable one of my PHP projects to log in via authentication with ADFS. I’ve recently updated this to talk directly with Azure AD, and have split this off into a separate project which I’ll share here.

Basically this works using oAuth2, browser sessions, a database and a couple of scripts, and on the Azure AD side you need to create an App Registration. Within this sample project the following flow happens:

  • User lands on index.php. If they do not have a session key cookie, one is generated, and this is stored in the database along with the page the user was attempting to access. They are redirected to login.microsoftonline.com to authenticate.
  • If you allowed authentication from any tenant, and used the common endpoint (rather than your specific tenant ID), the user may be asked to allow your app to access their account. If they are on their home tenancy, you will have already approved this for all users.
  • The user is redirected to the oauth.php file, where a background request is made back to login.microsoftonline.com to obtain a token. Once this has been successful, the user is redirected back to their original destination.
  • If the user lands on index.php and their session key cookie already exists, and exists in the database, and has not expired, they will be allocated that token’s data.
  • If the user lands on index.php with a session key cookie, but it is going to expire in the next 10 minutes, we will perform a refresh request in the background.
  • If the user lands on index.php with a session key cookie, but it’s expired, they are redirected back to login.microsoftonline.com – which may automatically log them back in, or may prompt, depending on their settings.

The code is available on my GitHub repository. The various files within are:

  • database.sql – the table used by this project. Create a database first, and note down the details (host, username, password, database).
  • inc/_config.inc – configuration file, put your database details in here, along with your tenant ID, client ID and client secret or certificate details from the Azure AD App Registration. There’s also an entry for URL which is the URL of your site, included rather than automatically determined incase your site is accessible by multiple URLs as this needs to match what you’ve entered into the App Registration. I’ve defined constants for all these settings, there is probably a better way to do this but they work.
    Note this file must be renamed config.inc.
  • inc/mysql.php – class used for accessing the database, nothing needs editing in here.
  • inc/auth.php – main authentication class – again nothing to edit in here.
  • www/index.php – sample page which will request login and then display your username and a logout link, along with a print of all the data retrieved during logon.
  • www/oauth.php – callback script, Azure AD login page returns you back to this script.
  • inc/oauth.php – some of the worker functions used by inc/auth.php and www/oauth.php

When putting this on a web server, the web root should be www and the inc directory should not be accessible from the web browser.

Note: You will need to have the PHP CURL library installed on the server for this to work. You will also need openssl if you want to use client certificates instead of a client secret – when I tested this on linux and Windows, it was already installed.

To create the App Registration, to to Azure AD > App registrations and click New registration. Fill out your site display name, select the account types, then enter the Redirect URI which will be https://your.domain.name/oauth.php. Make sure that you put https://your.domain.name (with no trailing slash) into _URL within config.inc. Click Register once done.

Screenshot of Azure AD Register an application screen, showing the display name and Redirect URI completed.
Register the application with Azure AD

A quick note on supported account types – if you use “common” as your tenant ID within config.inc, any account on any Azure AD tenant, plus personal (outlook.com etc) accounts can log in. If you use your actual tenant ID, then only accounts which are on your tenant (either as normal accounts, or as guests from other tenancies) can log in, but personal Microsoft accounts will work if they are guests on your tenancy, assuming you’ve set the supported account type in the App Registration accordingly.

Once you’ve finished creating the app, you should see the Overview screen. Copy the client ID and tenant ID, pasting them into _OAUTH_TENANTID and _OAUTH_CLIENTID in config.inc. The _OAUTH_TENANTID entry should be either your tenant (directory) ID for a single tenant app, or the word ‘common’  for the multi-tenant app.

App registration overview screenshot, showing client ID and tenant IDs
Copy the Application (client) ID and Directory (tenant) ID.

Now on the Certificates & secrets page, add a new secret and select the appropriate time. Don’t forget you will need to update this before it expires, so make a note in your calendar. Once done, copy the secret value and paste this into _OAUTH_SECRET within config.inc. Make sure _OAUTH_METHOD contains ‘secret’.

Alternatively you can use a certificate – which is the preferred route as it’s more secure. You will need the private key and certificate on the server file system (in a non-web-accessible location), and you’ll need to add the certificate on the Azure AD app registration. If you’re going down this route, you will need to set _OAUTH_AUTH_CERTFILE to the full path to the certificate file, and _OAUTH_AUTH_KEYFILE to the full path to the private key. _OAUTH_METHOD will need to be set to ‘certificate’.

Hopefully you should now be able to browse to your application and be prompted to log in. On your first go, you’ll be asked to allow permissions for everyone on your tenant (assuming you have the appropriate admin rights).

Permissions Requested login prompt
Tick the box to accept for all users on your tenant

 

Update (1st Sept 2021)

I’ve updated this to show some basic use of the Graph API. The default index.php page will now pull the logged on user’s profile photo and basic profile data. You’ll need to configure the _OAUTH_SCOPE constant in config.inc and make sure this includes “user.read” for this bit to work. It’s not required but if you want to read the logged on user’s directory entry, you can do.

There’s more detailed use of Graph API in my other PHP post, Graph Mailer, which demonstrates reading a mailbox and sending mail through Office 365. You’ll need to alter the scope in config.inc to add the appropriate permissions in if doing this in your own application, and query using the logged on user’s access token (show in this project) rather than the application’s own token (show in the Graph Mailer project).

Update (15th & 16th Oct 2021) – Restricting Access 

You can further restrict access to your application in Azure AD. Go to the Azure AD > Enterprise Applications page, find your app and then go to the Properties page. Toggle “Assignment required” to “Yes”. Now go to the Users and groups page, and add the users/groups you wish to have access.

You can further extend this by adding different roles to the application – in Azure AD > App Registrations, find your app and go to the App roles page. Create an app role for each role you want to assign, e.g. Admin and User:

Screenshot of Create app role
You can create many app roles to help control access within your application, in this sample project I’ve gone for Role.Admin and Role.User.

The display name is shown when you assign users in Azure AD, and the Value is what is shown in the PHP code on your application. When assigning users in the Enterprise Application screen you will be asked to select their role.

In the index.php sample, you will notice $Auth->userRoles referred to – this will be an array of user roles. You can use the checkUserRole function in auth.php to verify if a user holds the role required for a specific part of your application, i.e. if ($Auth->checkUserRole(‘Role.Admin’)) { //admin only code here }

 

Further Reading:

 

22 Replies to “PHP: Implement Azure AD login to your site”

  1. Thanks for working this out. I tried your code and everthing works fine (Sign in to AD etc) only if i try to open the index.php after Sign In i get some to many redirects errors. What can be the problem?

    Cheers,
    Eric

    1. Not sure – I’ve got this running on a couple of different servers embedded in some quite big projects so all I can think is it’s something to do with PHP config, maybe if the sessions aren’t working properly. If I get time I’ll see how it handles on a fresh install of the latest version PHP etc.

  2. For anybody having trouble with the endless loop, check your apache error logs. For me, it was the following error:
    PHP Fatal error: Uncaught Error: Call to undefined function curl_init()

    Need to install php curl.

    apt-get install php-curl

  3. Do you have any advice for this script running on IIS? I have PHP installed correctly, with the CURL dll enabled. I also get the error there are too many redirects. I tested the script on a true LAMP setup, and works, but my requirement is for IIS. Do you have any advice on what to look for?

    1. Alter oauth.php, after line 31 insert (after curl_exec, before curl_close) – updated on GitHub if it’s easier to just update the file:
      if ($cError = curl_error($ch)) {
      die($cError);
      }

      you should then see an error message when you try to log on – when I tried this on IIS I got:
      “SSL certificate problem: unable to get local issuer certificate”

      The fix for this is to download the root CA cert bundle and alter php.ini:

      curl.cainfo = c:\path\to\cacert.pem

      as per this article: https://martinsblog.dk/windows-iis-with-php-curl-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate/

      1. Hi Katy,

        Thanks for the update! I updated oauth.php from your GitHub, and fixed the CACERT issue. Unfortunately, I am still getting the redirect errors, but now it ends with this:

        AADSTS54005: OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token. Trace ID: 9b53e0f2-ef9e-4fe0-a041-ca6cb4792100 Correlation ID: d92cd4c6-6c06-4244-b1ff-88e5531a75e6 Timestamp: 2021-10-12 14:18:47Z

        I am running IIS 10, with PHP 7.4.13. This is a new install running in a VM for development purposes, and can be rebuilt, if needed.

        Is there anything else I’m missing? I am new to PHP development, and any advice is always welcome. Thanks!

      2. Make sure you clear the session cookie (in Edge, F12 dev tools > Application > right click the Cookies and clear), and drop the row from the database table, then try again, it’s most likely got upset at the process failing the first time while you sorted out the CACERT thing. I can’t think of any reason why it wouldn’t behave now that curl is working. Mine was Server 2019, IIS latest version, PHP 8.0 but the LAMP server I use is PHP 7

      3. I set up a blank Windows 2019 server, with IIS 10, PHP 8.0 installed via WebPI, and MySQL 8 with a fresh database. I’ve verified the curl plugin is active. I downloaded your code, and did the root CA cert fix. The only site on IIS is the default running your code. I’m sure the address I’m using is the same in my Azure registration. I am still getting the same errors as above. I see in MySQL blank entries, then the txtRedir path. I am running the test on Edge on the webhost itself. I’ve made sure I’ve cleared the cookies, and I’ve also tried to run this in InPrivate (Edge) mode to make sure no weirdness is occurring. I’ve also tried to access the site from another computer, and it’s stuck in a loop asking for my Azure account name. Is there anything else, I really could be missing?

        I can make this work all day with my LAMP setup, which is cool…

        If I am asking too many questions, please let me know.

      4. I can’t work out what could be going wrong as it works fine on my 2019 VM with all the default settings once that cacert issue was sorted (PHP 8 installed via the webPI). I think that error’s coming if it tries to get the auth token multiple times (as you can only redeem the auth code once), which is what oauth.php does. Maybe stop the redirect on line 45, attempt to log in and check the database entry has updated in the same way it does on LAMP.

        When you access index.php it should:
        – Add a row to the table with the redirect (index.php), and send you to MS login page
        – MS login sends you to oauth.php which redeems the auth code to get a token, and updates the DB table with this
        – Redirects you back to where you were going (index.php) and you’re now logged in.

    2. I stopped the redirect on line 45, and checked the database entries. As described, the row inserts with the redirect (txtRedir). After the Azure credentials, oauth.php finishes, and the database updates the row, txtRedir disappears, and gets the token. All this seems to work good. When I remove the redirect, that’s when it gives the error, and populates multiple rows.

      I almost feel there is something I’m missing in IIS when I set up the default site.

      1. I meant “When I re-added the redirect” instead of “When I remove the redirect”. Apologies.

      2. Okay, so it sounds like when it redirects back to index.php, and runs through the code in auth.php it’s not reading your session cookie and thinks you need a new one and haven’t logged in (auth.php line 30, and lines 81-90). I guess to test you could hard code the redirect in oauth.php line 45 to something like index2.php, where you just get it to output the content of $_SESSION with die(print_r($_SESSION, true)) or similar, to make sure the session cookies are actually working properly.

      3. I’ve reworked the code a bit, we don’t store the decoded access_token (as txtJWT) in the database anymore as that was causing an issue using personal accounts (they don’t provide the data in the same way as Azure AD accounts do), so it may be that it works on IIS for you now with those changes.

      4. I tried the new code, but still getting the redirect errors. Cleared cache to make sure. Rebuilt the DB table with the included sql file. I haven’t had a chance to isolate the issue, as you advised. My app is still being developed on my LAMP setup until I can hash out this issue.

  4. This looks absolutely perfect for what we need, but I’m having a slight problem. When users visit the first site (not logged in), they are given the following error:

    Warning: session_start(): Cannot start session when headers already sent in /home/datcomme/public_html/test/inc/auth.php on line 27

    Warning: Cannot modify header information – headers already sent by (output started at /home/datcomme/public_html/test/inc/config.inc:36) in /home/datcomme/public_html/test/inc/auth.php on line 99

    Do you know what I need to do to fix this please?

    Thank you.

  5. I am getting the same errors as callum, I have checked config.inc and there are no blank lines before or after the php tags

    PHP Warning: session_start(): Cannot start session when headers already sent in /var/www/co.uk/cover/inc/auth.php on line 30
    PHP Warning: Cannot modify header information – headers already sent by (output started at /var/www/co.uk/cover/inc/auth.php:155) in /var/www/co.uk/cover/inc/auth.php on line 97

  6. Katy, thank you so much for this code. I think I am close! but I’m getting an error in the inc/oauth.php file on line 28

    $oauthRequest = $oAuth->generateRequest(‘grant_type=authorization_code&client_id=’ . _OAUTH_CLIENTID . ‘&redirect_uri=’ . urlencode(_URL . ‘/oauth.php’) . ‘&code=’ . $_GET[‘code’] . ‘&code_verifier=’ . $sessionData[‘txtCodeVerifier’]);

    error says:

    Warning: file_get_contents(/path/to/certificate.crt): failed to open stream: No such file or directory

    any idea what could be causing this?

    Many thanks!
    Peter

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.