

AzureAD as an OpenID Connect (OIDC) and OAuth provider
source link: https://blogs.sap.com/2021/08/31/azuread-as-an-openid-connect-oidc-and-oauth-provider/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

AzureAD as an OpenID Connect (OIDC) and OAuth provider

First things first:
- In the previous instalment I demonstrated Keycloak as an OpenID Connect (OIDC) provider.
- This instalment is dedicated to having AzureAD as an OpenID Connect (OIDC) provider for third-party applications implemented with SAP Kyma functions.
- Indeed, AzureAD is the Microsoft identity platform that can act as an OpenID Connect (OIDC) provider so you can create OIDC applications (so called clients) for password-less user authentication.
Good to know:
- You can use AzureAD as an OpenID Connect (OIDC) and OAuth provider with Azure Free tier account (Pay-As-You-Go subscription) or with a trial account.
- The OIDC/OAuth protocol supports a number of grant types that can be implemented to authenticate a user;
- For the sake of clarity I shall be using the authorization code grant that is well supported by AzureAD.
- Kyma functions are well-suited for rapid development/prototyping of cloud native micro-services.
Disclaimer:
- Please note all the code snippets below are provided “as is”.
- All the x509 certificates, bearer access and refresh tokens and the likes have been redacted.
- Images/data in this blog post is from SAP internal sandbox, sample data, or demo systems. Any resemblance to real data is purely coincidental.
Putting it all together.
If you were new to Azure portal it might appear relatively “overwhelming” at the first sight. Actually it is really “overwhelming”. But stay cool man, AzureAD offers tons of good docs and resources though.
For the sake of time, I suggest to start with this very short knowledge base article to help you understand the basic concepts of OpenID Connect protocol with AzureAD:
Quovadis-Web.
Let’s have a closer look on how to use AzureAD as an IODC provider for user sign-in and an OAuth provider for granting access to Microsoft Graph API.
I did set up a Quovadis-Web application with my free AzureAD personal account (I followed the official application registration guide.)
In a nutshell, with our Quovadis-Web application, we shall obtain both a signed user JWT token and an OAuth bearer access token.
The first can be used in saml bearer assertion flows to propagate a signed user identity to any cloud native LOB application of the likes of SuccessFactor, S/4HANA Cloud, Analytics Cloud, Commerce Cloud, etc. and the latter can be used with MS Graph API.
Image: source
1. Create an OIDC client (application) with AzureAD.
Please find below a set by step Quovadis-Web application registration screenshots.
a. Goto https://portal.azure.com/#home
.
This is the azure portal home screen where you can access your subscription details and locate the AzureAD service.
b. Goto Azure Active Directory service: https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview
From the overview section you can look up your AzureAD directory tenant id, as depicted below:
c. Goto AppRegistration: https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps
You can start a new app registration and/or look up/modify features of other registered applications you own.
Through the registration process you can add your custom branding features including a graphical logo, home page url, and any applicable T&C and privacy statements that you want your users to abide by.
Next, you will need to select the application type or types , for instance Web and SPA, and define one or more callbacks (redirect URLs) if applicable.
Indeed, depending on the authentication grant method you may need to define one or more callbacks;
For instance with the authentication code grant a callback url will allow your application to retrieve the authorisation code that is required to retrieve the bearer access token.
d. Eventually, after a successful Quovadis-Web registration we shall have obtained the following information required to implement the OIDC user sign-on programmatically:
// the app client secret may be rotated at will at any point of time
// from the AzureAD App registration wizard....
// however the client id (app id) is unmutable...
app_name = Quovadis-Web
client_id = '8e4ed817-***************************'
client_secret = 'c91******************************'
2. Get OIDC provider (issuer) metadata.
OIDC providers are also commonly being referred to as issuers.
The issuer link is https://login.microsoftonline.com/{tenant}/v2.0/
Thus the OIDC metadata can be accessed by appending the .well-known/openid-configuration suffix to the issuer url, for instance:
https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
.
For instance, wee could use a AzureAD stock personal tenant id:9188040d-6c67-4c5b-b112-36a304b66dad
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration
{
"token_endpoint": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/oauth2/v2.0/token",
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"private_key_jwt",
"client_secret_basic"
],
"jwks_uri": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/discovery/v2.0/keys",
................(truncated)..............................................................
}
3. Retrieve a user JWT and Auth bearer access token
Finally let’s have some fun and joy and write some code.- The only pre-requisite is to have access to a SAP Kyma cluster either via SAP Business Technology Platform (BTP) or on top of SAP Gardener or on your personal SAP BTP trial account.
- And because the code is implemented with a kyma function all that is required is a browser with the internet access.
Once registered, the Quovadis-Web app communicates with the Microsoft identity platform by sending requests to the oauth endpoints as follows:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
where {tenant} needs to be substituted with your AzureAD tenant id….as shown above.
Good to know:
- As we shall be leveraging the open-id library so the code is pretty much the same regardless of the OIDC provider…
-
tokenSet.id_token holds the user JWT token
-
tokenSet.access_token holds the bearer access token
-
//-----------------------------------------------------------------------------------------------------------------------------------
// https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/{client_id}
//-------------------------------------------------------------------------------------------------------------------------------------
// store the code_verifier in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.
//
const code_verifier = generators.codeVerifier();
//
const credentials_azure = {
client: { // issuerUrl: https://login.microsoftonline.com/{tenant}/v2.0
id: '8e4ed817-*************************',
secret: 'c91*****************************'
},
auth: {
authorizeHost: 'https://login.microsoftonline.com/{tenant}',
authorizePath: 'oauth2/v2.0/authorize',
tokenHost: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
//tokenPath: '',
},
options: {
authorizationMethod: 'body'
}
};
//--------------------------------------------------------------------------------------------------------
// https://www.npmjs.com/package/openid-client
// https://github.com/jcawley5/oidc-sample-app/blob/master/app/auth/sapOIDC.js
const axios = require('axios')
const { Issuer } = require('openid-client');
const { generators } = require('openid-client');
const { TokenSet } = require('openid-client');
const scopes_supported_azure = "openid email user.read mail.read";
//const scopes_supported = "openid eamil https://graph.microsoft.com/.default"
var client_azure;
const azure_issuerUrl = 'https://login.microsoftonline.com/{tenant}/v2.0';
const redirecturi_azure = 'https://poster.<client-cluster>/auth/web';
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function discover_openid_issuer(issuerUrl) {
try {
issuer = await Issuer.discover(issuerUrl);
console.log("Discovered issuer %s %O", issuer.issuer, issuer.metadata);
return issuer;
} catch (error) {
console.log(error);
return null;
}
}
//-------------------------------------------------------------------------------------------------------------------------------------
function init_issuer_clients(azureIssuer) {
//token_endpoint_auth_method: client_secret_basic - may cause an issue due to the encoding of the client_secret
//if special characters exist
//xsuaa: client_secret_post
//sapias:client_secret_basic
//https://tools.ietf.org/html/rfc6749#section-2.3.1
//
if (azureIssuer !== null) {
client_azure = new azureIssuer.Client({
client_id: credentials_azure.client.id,
client_secret: credentials_azure.client.secret,
redirect_uris: [redirecturi_azure],
response_types: ["code"],
token_endpoint_auth_method: "client_secret_post",
});
}
console.log(client_azure);
return client_azure;
}
//-------------------------------------------------------------------------------------------------------------------------------------
function getmetadata(event) { // ?issuer=azure
if (typeof (event.extensions.request.query.issuer) !== 'undefined') {
if (event.extensions.request.query.issuer === 'azure') {
return JSON.stringify(azureIssuer.metadata, null, 2);
}
}
return JSON.stringify(sapIssuer.metadata, null, 2);
}
//-------------------------------------------------------------------------------------------------------------------------------------
async function getuserinfo(event) { // ?issuer=azure
let refreshParams = {
client_id: credentials_azure.client.id,
client_secret: credentials_azure.client.secret,
scope: scopes_supported,
};
let client = client_azure; // current client
if (typeof (event.extensions.request.query.issuer) !== 'undefined') {
if (event.extensions.request.query.issuer === 'azure') {
// https://graph.microsoft.com/v1.0/me
try {
let accessToken = process.env[azureConfigMap];
console.log("serialized access token: ", accessToken);
accessToken = JSON.parse(accessToken);
console.log("deserialized access token: ", accessToken);
tokenSet = new TokenSet(accessToken);
const response = await axios.get('https://graph.microsoft.com/v1.0/me', {headers : { "Authorization": 'Bearer ' + tokenSet.access_token}});
return JSON.stringify(response.data, null, 2);
}
catch (err) {
console.log("An error occured: ", err);
return JSON.stringify(err, null, 2);
}
}
}
try {
let accessToken = process.env[azureConfigMap];
console.log("serialized access token: ", accessToken);
accessToken = JSON.parse(accessToken);
console.log("deserialized access token: ", accessToken);
tokenSet = new TokenSet(accessToken);
const userinfo = await client.userinfo(tokenSet);
console.log('userinfo %j', userinfo);
return JSON.stringify(userinfo, null, 2);
}
catch (err) {
console.log("An error occured: ", err);
return err.message;
}
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function authorizationcode_openid_client(event) {
let client = client_keycloak; // current client
let scope = scopes_supported_azure; //scopes_supported;
try {
const code_challenge = generators.codeChallenge(code_verifier);
let authorizationUri = client.authorizationUrl({
scope: scope,
access_type: 'offline',
prompt: 'consent',
//resource: redirect_url,
code_challenge,
code_challenge_method: 'S256',
});
console.log("authorizationcode_openid_client: authorizationUri: " + authorizationUri);
event.extensions.response.redirect(authorizationUri);
return authorizationUri;
} catch (error) {
console.log(error);
}
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function callback_openid_client(event, redirecturi_id = redirecturi_azure) {
// https://github.com/panva/node-openid-client/blob/main/docs/README.md#clientcallbackredirecturi-parameters-checks-extras
let client = client_keycloak; // deafult client
// Returns recognized callback parameters from a provided input.
const params = client.callbackParams(event.extensions.request);
console.log ('params: ', params);
try {
tokenSet = await client.callback(redirecturi_id, params, { code_verifier }, { exchangeBody: {access_type: 'offline',}});
console.log('received and validated tokens %j', tokenSet);
console.log('validated ID Token claims %j', tokenSet.claims());
console.log("callback_openid_client: " + JSON.stringify(tokenSet));
// you might want to securely save the token in a secret place...like a config map for instance or in a database or in a vault...
//await k8spatchcm();
// for the sake of simplicity the token is returned both as a param to another endpoint and a nd as a safe server-side cookie
let token_cookie_header = 'token_azure';
event.extensions.response.clearCookie(token_cookie_header);
event.extensions.response.cookie(token_cookie_header, tokenSet.id_token, { httpOnly: true, secure: true, sameSite: 'none' }); // tokenSet.access_token
event.extensions.response.set(token_cookie_header, tokenSet.id_token); // tokenSet.access_token
return event.extensions.response.redirect('/logonresponse_openid?token=' + tokenSet.id_token); // tokenSet.access_token
}
catch (err) {
console.log("An error occured: ", err);
return err;
};
}
//-------------------------------------------------------------------------------------------------------------------------
//
function logonresponse_openid(event) {
console.log("inside logonresponse_openid...");
let token = "";
// is token passed via a parameter from the callback ?
if (typeof (event.extensions.request.query.token) !== "undefined") {
token = event.extensions.request.query.token;
}
else {
console.log(event.extensions.request.get('cookie'));
var cookies = cookie.parse(event.extensions.request.get('cookie') || '');
token = cookies.token_openid;
//let accessToken = cookies.serialized;
}
console.log("logonresponse_openid: token: ", token);
return token;
}
and here you go: the entry point of a kyma function:
//
var azureIssuer, initialized = false;
//--------------------------------------------------------------------------
//
module.exports = {
main: async function (event, context) {
if (initialized === false) {
initialized = true;
azureIssuer = await discover_openid_issuer(azure_issuerUrl);
client = init_issuer_clients(azureIssuer);
}
switch (event.extensions.request.path) {
case '/openid': {
return authorizationcode_openid_client(event);
}
case '/auth/web': {
return callback_openid_client(event, redirecturi_azure);
}
case '/logonresponse_openid' : {
return logonresponse_openid(event);
}
case '/getmetadata' : {
return getmetadata(event);
}
case '/getuserinfo' : {
return getuserinfo(event);
}
}
}
}
}
Using the kubectl port-forward mechanism we can run any kyma function endpoint locally without the need of exposing the endpoints to public internet.
http://localhost:8080/getuserinfo
{
"sub": "<sub>",
"name": "<username>",
"family_name": "<family_name>",
"given_name": "<given_name>",
"picture": "https://graph.microsoft.com/v1.0/me/photo/$value"
}
And this is how we can acquire a user JWT token (and the MS Graph access bearer token).
The endpoint will print out the id_token but you can easily modify to print out the access_token as well.
http://localhost:8080/openid?issuer=azure
{
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzIxxxxxxxxxxxxxx.eyJhdWQiOiI43mqZZUzxxxxxxxxxxxxxxxxxxxxxxx
}
On successful call return we may look up the user JWT token using this handy tool: https://jwt.ms/
, as depicted below:
https://jwt.ms/
Next, we can look up the user information via a call to MS Graph API
http://localhost:8080/getuserinfo?issuer=azure
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
"@odata.id": "https://graph.microsoft.com/v2/{tenant}/directoryObjects/{id}/Microsoft.DirectoryServices.User",
"businessPhones": [],
"displayName": "<displayName>",
"givenName": "<givenName>",
"jobTitle": null,
"mail": null,
"mobilePhone": null,
"officeLocation": null,
"preferredLanguage": "en",
"surname": "<surname>",
"userPrincipalName": "<userPrincipalName>",
"id": "<id>"
}
4. Use the user JWT to get access to oauth protected resources with saml bearer assertion and BTP destination service
The main idea is to leverage the acquired user JWT token as the user’s identity to get to access to some third-party oauth protected asset.
It is assumed that the user’s identity we are aiming to propagate does exist on the target system; otherwise the access will be denied.
The user identity verification is achieved by sending the target system a signed saml assertion with the client application user claim. If the identity is asserted the target system will issue a bearer access token to allow for the remote resource access.
SAP BTP destination service can help retrieving the bearer access token so you can call your target system REST or ODATA API for instance:
https://{apim tenant}/oem-azure/opendocument/?story=ateam-isveng&issuer=azure
Good to know:
- If you are interested in how to implement the saml bearer assertion flow with Analytics Cloud or SuccessFactors or S/4HANA Cloud and the likes, please refer to the this guide, namely Quovadis or how to find your destination?
Additional resources.
Recommend
-
7
OAuth 2.0 and OpenID Connect Overview To decide which authentication flow is best for you based on the type of application that you are building, you first need to understand OAuth 2.0 and OpenID Connect and how you can implement the...
-
11
Step-up authentication with OAuth and OpenID Connect Implementing step-up authentication doesn’t need to involve your applications orchestrating calls to multiple complex APIs. Instead, by leveraging the features already...
-
4
Migrating oidc-client-js to use the OpenID Connect Authorization Code Flow and PKCE Recently, there’s been a bit of a palaver around a draft...
-
6
Why Developers Do Care About OAuth and OpenID Connect Recently, Okta released an article titled “Nobody Cares About OAuth or OpenID Connect” that authoritatively stated that “Developers don’t care about OAuth or OpenID Co...
-
10
SPA Authentication using OpenID Connect, Angular CLI and oidc-client Scott Brady 03 August 2017 ・ Angular ・ Upd...
-
5
OIDC(OpenID Connect) 简介 最近了解了一下认证体系中的 OIDC ,整理成一篇博客,主要包含 OIDC 中的概念,流程以及使用。在介绍这些之前,我们先要弄清楚 两个名词 Authorization 和 Authentication。 Authorization vs Authentication
-
5
Securing your SignalR applications with OpenID Connect and OAuth
-
5
OpenID Connect (OIDC) and OAuth 2.0 are two popular protocols you can use for authorization and authentication on the web. Both protocols allow users to securely access resources, but they work in different ways a...
-
12
Yogananda Muthaiah February 25, 2023 1 minut...
-
12
OAuth made easier with Feathers v4/v5, OpenID Connect and KeycloakIn this article we detail a running setup to authenticate users in your Feathers app based on either their account from public identity provi...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK