Notice
The examples and use cases described here are intended to show the different ways SURF Research Access Management can be used and connected to application. These examples and use cases are not always validated by SURF.
Microsoft Azure uses Entra Id (previously known as Active Directory) for authentication and authorization. Entra Id can be used to give access for both external and internal users. (A copy of) the profile of the users however will be stored in Entra Id.
One of the limitations with Entra Id this is that you cannot use external identity providers by default. For this Microsoft has created Azure AD B2C: Azure AD B2C is an extension of Entra Id /Azure AD designed specifically for external-facing applications and scenarios involving consumers from external providers. It focuses on providing identity services for applications that need to authenticate and authorize users who are not part of the organization's employee base. Azure AD B2C is suitable for applications that serve customers, clients, or partners, allowing them to sign up, sign in, and manage their identities.
Before you can create these application registrations you need to set up an Azure AD B2C environment.
Note on screenshots
Azure frequently updates the look&feel of the portal user interface. The screenprints provided in this document may be subject to change in the Azure Portal as of today. So please use these scrieenprint with that in mind. The logic will probably stay consistent, the way it looks might be different.
Note on complexity
This document might seem quite overwhelming for a fairly trivial exercise "Authenticate at a Azure Application using an external (SRAM) identity provider, with just-in-time identity creation in the Azure AD"
But the reason for all this to be necessary is because many required ingredients are quite hidden in both the Microsoft documentation as wel in the Portal GUI.
The good news is, we closed to loop and finished the exercise successfully.
Get an Azure AD B2C environment
This paragraph describes how to create a Azure AD B2C environment. It is more for convenience for development and demo purposes, than real life since you might expect that a customer already has such an environment.
To get a free Azure environment you can sign up for a free Azure account at https://azure.microsoft.com/en-us/free/ option Start Free. You’ll need a credit card, which is only used to validate you as a person. Unless you move to Pay as you go your environment is free of charges.
When you sign up you create a subscription, which is basically a user directory with the option to add resources. The first time you will be asked to create the trial with $200 free credit and certain popular services are free of charges for 12 months.
If you use an existing tenant you need to have global admin rights to be able to manage the Entra Id directory.
Creating the Azure AD B2C directory
Within your subscription go to the top search bar and find Azure AD B2C. The first time you get the option to create a new Azure AD B2C tenant.
When you have created it, you create essentially a new tenant with a new directory, but this time it is based on Azure AD B2C. To switch to this newly created subscription go to your account in the top right where you’ll find a Switch directory option.
After you have switched to the new B2C directory you’ll find B2C simply by typing Azure AD B2C in the search bar.
Navigation in Azure
As a quick reference, here are some guidelines where to find what in Azure. The colours will be used later in the document to give you directions where to find an option.
In the image below you see three sections:
Red: Here you go to resource categories. If your category isn’t mentioned you can find it by using the search bar in the top. Under All resources you’ll find any resource that is created in the subscription.
Blue: This is the blade that gives the different sections (tabs) within a specific resource.
Green: This is the form that belongs to the section of the specific resource.
In our image we see the Overview section of the Azure AD B2C resource.
Configuring Azure AD B2C for SRAM
In the Azure AD B2C resource you need to perform a series of tasks to enable SRAM as provider.
You need to add SRAM as Identity Provider for Azure B2C.
You need to create an App Registration that can use the SRAM Identity Provider Connection.
You need to create a client secret on the app registration.
You need to define user flows so you application can call certain flows.
Add SRAM as a allowed Identity Provider
Navigate to Azure AD B2C (red section)
Go to Identity Providers (blue section)
Press New OpenID Connect Provider (top of green section)
Enter the details for SRAM: (see image below)
The Client ID and Secret are the ones retrieved from your SRAM Service.
Note: name and well known configuration will be read-only after you press save.
Now we have completed the Identity Provider in a Azure B2C directory, we proceed with an App Registration that will make use of that IDP
App Registrations
Within Entra and B2C you have the concept of app registrations. An app registration is comparable to a client in Keycloak or a service in SRAM.
App registrations in Entra itself cannot have an External Identity provider, nor can you set mapping, flows etc.
In B2C you can create an app registration using an external identity provider:
Create the application registration
Go to App Registrations (within Azure AD B2C, blue section)
Choose New registration (top of green section)
Give it a meaningful name, remember this is comparable to an SRAM Service.
Set the supported accounts to:
Accounts in any identity provider or organizational directory (for authenticating users with user flows)Check “Grant admin consent to openid and offline_access permissions” under Permissions.
Press Register.
After creating the app registration you van specify the redirect urls under Authentication.
If you tick Access Tokens under Implicit grant and hybrid flows you can use https://jwt.ms (web) as redirect URL to analyse the tokens.
Add a client secret on the app registration
Go to App Registrations (within Azure AD B2C, blue section)
Go to Certificates & Secrets (blue section)
Create under Client secrets (red section) a new secret. You can set expiration dates. Copy the value. This is the actual client secret you need to use in flows.
Add a User flow for signing in (or up)
Go to User Flows (within Azure AD B2C, blue section) under Policies
Choose New user flow (top of green section)
Choose Sign up and sign in (green section) with Recommended version
Name the flow (it will be automatically prefixed with B2C_1_)
Check SRAM as the Custom Identity Provider
In the bottom of this screen you can specify the claims that are collected on sign up. Here you can get the Identity Provider Access Token.
You can also define extra properties here, there are some standard but you can for example add a field that asks for a property that is specific for your organization.
Note
If you allow more identity providers or local accounts you’ll get a login screen that has a default interface like this:
Testing a flow
In the overview of an user flows you can select Run User Flow. You can set the redirect url to https://jwt.ms to analyse the tokens. You must have enabled Access Tokens under Implicit grant and hybrid flows on Authentication section of the App Registration.
Use case : using SRAM authentication on an Azure App Service (web application) via Azure B2C
You can write code within an web application that uses the OpenID Connect protocol to directly connect to SRAM. However, if you do not want to write code within your application or when you have no option to change it’s authentication from within the application you can use the B2C environment connected to SRAM.
Assume you have an application deployed to Azure and there is no authentication build in. This means that anyone can access the application on the internet via it’s web address.
Adding authentication to the app service
Navigate in Azure to your website (app service)
choose Authentication (blue section).
Add identity provider
Choose OpenID Connect as Identity Provider
Give it a proper name (you need this in the callback url at OpenID provider name)
For metadata entry you can specify an url in the form like
https://<subscription>.b2clogin.com/<subscription>.onmicrosoft.com/<policy-name>/v2.0/.well-known/openid-configuration.
<subscription> is the name of the B2C tenant, <policy-name> is the name of the user flow.Enter the client ID (Application ID), this can be found on the Azure AD B2C App Registrations your app registration Overview tab.
Enter the client secret (you have created this or you can create this on Azure AD B2C App Registrations your app registration Client & Secrets
Note: despite the pans with title ‘Scopes’ you cannot do anything with that since it is defined in your Identity Provider settings.
The final thing you need to do is to add an redirect url to your app registration that reflects your application. Your redirect url is https://<your application url>/.auth/login/< OpenID provider name>/callback.
Restart your application.
Note it may take a bit of time before the authentication takes place. Especially if you change the names of the providers it might take a while before you see your changes reflected.
Signing in via SRAM and B2C
Depending on the attributes you request the user to enter on sign up you get a form like this:
If you do not request additional attributes you do not get a form and you will be directed immediately to your application.
Testing and analysis of the user flow
After you have setup the SRAM provider in Azure B2C, you can test the user flow. It is interesting to play a bit with the different options and analyse the result.
Testing a flow
In Azure B2C go to Policy -> User flows
When you click a user flow you can edit many properties and test it.
Analysing the access token
If you press Run user flow you will get an option to test against an app registration that you previously created. Pick the one where you added https://jwt.ms as a valid redirect url (or add it to one).
If you now run the user flow, you will be able to sign in via SRAM and you will be directed to https://jwt.ms
In here you will see the contents of the Access token return by Azure. In that token you see an idp_access_token. This is the token from SRAM and if you copy paste that you’ll see the data from SRAM.
Getting SRAM specific attributes
SRAM has specific attributes that can be interesting for further use. See also Attributes in SRAM - SURF User Knowledge Base - SURF User Knowledge Base.
Assume we want to add eduperson_principal_name and eduperson_entitlement we go to Azure B2C -> Manage -> User Attributes and add them.
However this is not enough. If you look at the attribute definitions on the Surf wiki you’ll notice that these two fields exist in different scopes. These scopes are not given unrequested so we need to make these specific scope requests.
Go back to you Identity Provider settings under AzureB2C and go to your OpenID Connect provider for SRAM. Click it and in the opening side panel you can specify these two extra scopes.
Now you can run the user flow again and when you analyse the idp_access_token you get both values returned from SRAM.
Advanced mapping, transforming and controlling the flows
In Azure B2C you can set many options via the interface but you can even do more using Technical Profiles. These are xml documents that describe the complete flow, attributes and you can perform complex transformations, mappings and flows. Basically anything you can control pretty much anything. However it is a very technical and challenging part of Azure B2C. You can upload these profiles and find more information via Azure B2C Identity Experience Framework.
Another good resource on this topic can be found in the community pages at Concepts - Azure AD B2C (azure-ad-b2c.github.io)
Deciding when to use when you authenticate via SRAM (OIDC) in Azure
Azure has several ways of securing and it might be hard to decide which one. The problem is that there is not written in stone which way should be used in what situations.
When we host an application in an App service in Azure we can authenticate via the following ways:
Within applications
Within your application you write the code. The limitations are just the limitations of the programming language.
Pros:
No knowledge of Azure Authentication needed, because you do not use it.
Cons:
Every app needs to have it’s build in code for connecting to SRAM.
If you have many application in different languages you have to deal with updates, security patches etc. yourself.
Using the authentication options in Azure on App service
You can directly add SRAM (OIDC) authentication in Azure on the App Service.
Pros:
Multiple applications can be set up in an easy way.
You only need to register one redirect URL with SURF for SRAM. You can manage the redirects for the applications yourself.
Cons:
You need to set-up the Identity provider on every app. If there is a change in for example the client-secret you need to change this in all applications.
Using the authentication options in an Azure App registration
Pros:
By using one authentication layer, many applications can use that, so you only need to maintain one.
Cons:
Only Microsoft accounts (M365) and Microsoft Personal accounts (Hotmail, live, Xbox,..) are supported, so you can’t use this option for SRAM.
Using the authentication options in in an Azure App registration within Azure B2C.
Pros:
You have the pros from the options above.
You can really finetune the flows and the interfaces.
You can use combinations of signing in with for example SRAM, local accounts and if wanted several social media accounts.
You can add mapping from SRAM to Azure and to you applications.
Cons:
It has a very steep learning curve.
It needs to run in a separate Azure subscription.
Differences in Azure authentication
Azure App authentication | Azure Entra ID Authentication | Azure B2C Authentication | |
Multiple apps? | Yes, but maintenance | Yes | Yes |
Multiple providers needed | Yes | No | Yes |
Attribute mapping needed | No | No | Yes |
Custom interfaces etc. needed | No | No | Yes |
non-Microsoft providers (like SRAM) needed? | Yes | No | Yes |
Note in this table we didn’t mention option one: Within applications, since that is unrelated to Azure Authentication.
B2C SRAM Custom Policy
In order to inspect the user_info endpoint of SRAM OIDC Provider we need to step away from the standard OIDC Claims Provider and write our own OAuth2-based Custom Policy.
The next picture describes the place of the custom policy and it's contents with respect to the Azure B2C tennant:
To start with Custom Policies, Microsoft has in-depth technical documentation about their use and configuration.
In this documentation, a reference is made to the Custom policy starter pack. This starter pack contains a set of XML files that will serve as a base for our SRAM Custom policy.
The start point for our Custom policy is the Social Accounts directory, which contains the following files:
TrustFrameworkBase.xml
TrustFrameworkLocalization.xml
TrustFrameworkExtensions.xml
SignUpOrSignin.xml
These four files are chained together via a BasePolicy declaration element in the top of the file (except TrustFrameworkBase.xml). For our purpose, we are going to replace SignUpOrSignin.xml and remove TrustFrameworkExtensions.xml from the chain, to simplify the editing and uploading process:
TrustFrameworkBase.xml
TrustFrameworkLocalization.xml
- SRAM.xml
The TrustFrameworkBase.xml policy file consists of Microsoft pre-cooked Azure B2C Custom claims, Technical profiles and Claims transformations that can later be used in RelyingParty definitions and UserJourneys. But before we upload the TrustFrameworkBase.xml and TrustFrameworkLocalization.xml files, we need to modify the TennantId in the TrustFrameworkPolicy attributes:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="<Your TennantId>.onmicrosoft.com" PolicyId="B2C_1A_TrustFrameworkBase" PublicPolicyUri="http://<Your TennantId>.onmicrosoft.com/B2C_1A_TrustFrameworkBase">
Before we continue, we need to define four new keys in the Policy Keys interface:
- B2C_1A_TokenSigningKeyContainer (signing)
- B2C_1A_TokenEncryptionKeyContainer (encryption)
- B2C_1A_AdminClientEncryptionKeyContainer (encryption)
- B2C_1A_SRAMSecret (signing)
Then, upload TrustFrameworkBase.xml and TrustFrameworkLocalization.xml. If there are errors during upload, inspect them and correct as necessary.
Our custom SRAM.xml policy consists of five elements, contained in a root TrustFrameworkPolicy element. Do not forget to replace the <Your TennantId>
placeholder:
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="<Your TennantId>.onmicrosoft.com" PolicyId="B2C_1A_UserInfo" PublicPolicyUri="http://<Your TennantId>.onmicrosoft.com/B2C_1A_UserInfo"> <BasePolicy> <BuildingBlocks> <ClaimsProviders> <UserJourneys> <RelyingParty> </TrustFrameworkPolicy>
To create the chain, we need to point to the TrustFrameworkLocalization policy in our BasePolicy. Do not forget to replace the <Your TennantId>
placeholder:
<BasePolicy> <TenantId><Your Tennant Id>.onmicrosoft.com</TenantId> <PolicyId>B2C_1A_TrustFrameworkLocalization</PolicyId> </BasePolicy>
The starting point of this custom policy is the RelyingParty definition. This element describes how Azure will output claims to the regstered apps and what UserJourney will start.
<RelyingParty> <DefaultUserJourney ReferenceId="SRAMSignUpOrSignIn" /> <Endpoints> <!--points to refresh token journey when app makes refresh token request--> <Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" /> </Endpoints> <TechnicalProfile Id="PolicyProfile"> <DisplayName>PolicyProfile</DisplayName> <Protocol Name="OpenIdConnect" /> <OutputClaims> <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" /> <OutputClaim ClaimTypeReferenceId="givenName" /> <OutputClaim ClaimTypeReferenceId="surname" /> <OutputClaim ClaimTypeReferenceId="email" /> <OutputClaim ClaimTypeReferenceId="city" /> <OutputClaim ClaimTypeReferenceId="usertype" /> <OutputClaim ClaimTypeReferenceId="voPersonExternalID" /> <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" /> <OutputClaim ClaimTypeReferenceId="identityProvider" /> <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" /> </OutputClaims> <SubjectNamingInfo ClaimType="sub" /> </TechnicalProfile> </RelyingParty>
The most important part of this element is the default UserJourney (SRAMSignUpOrSignIn) and the OutputClaims that will later be available to the OAuth2 id_token for the registered apps.
Next, we define the UserJourneys, mostly copied and adjusted from the example SignUpOrSignin.xml policy.
<UserJourneys> <UserJourney Id="SRAMSignUpOrSignIn"> <OrchestrationSteps> <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin"> <ClaimsProviderSelections> <ClaimsProviderSelection TargetClaimsExchangeId="SRAMExchange" /> </ClaimsProviderSelections> </OrchestrationStep> <!-- SRAM OAuth2 with UserInfo --> <OrchestrationStep Order="2" Type="ClaimsExchange"> <ClaimsExchanges> <ClaimsExchange Id="SRAMExchange" TechnicalProfileReferenceId="SRAM-OAUTH-UserInfo" /> </ClaimsExchanges> </OrchestrationStep> <!-- Always update the user with SRAM attributes, outputs objectId --> <OrchestrationStep Order="3" Type="ClaimsExchange"> <ClaimsExchanges> <ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" /> </ClaimsExchanges> </OrchestrationStep> <!-- Read any extra attributes from Entra ID --> <OrchestrationStep Order="4" Type="ClaimsExchange"> <ClaimsExchanges> <ClaimsExchange Id="UserReadUsingObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" /> </ClaimsExchanges> </OrchestrationStep> <OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" /> </OrchestrationSteps> <ClientDefinition ReferenceId="DefaultWeb" /> </UserJourney> </UserJourneys>
This policy references the standard pre-cooked api.signuporsignin CombinedSignInAndSignUp entry point, SRAM-OAUTH-UserInfo as our SRAM OAuth2 ClaimsExchange Technical Profile. The AAD-UserWriteUsingAlternativeSecurityId is slightly adjusted to not fail on existing users, so that SRAM claims always overwrite AAD claims. The AAD-UserReadUsingObjectId is adjusted for allowing city and usertype claims the default from TrustFrameworkBase.xml. Overriding Technical Profiles happens when the Id of the profile is exactly the same as a previously defined profile, this looks like inheriting classes and then overriding methods in a custom implementation of the class.
Besides introducing a new OAuth2 Technical Profile, we also overload some existing AAD Technical Profiles in the new Claims Providers:
<ClaimsProviders> <ClaimsProvider> <DisplayName>Azure Active Directory</DisplayName> <TechnicalProfiles> <TechnicalProfile Id="AAD-UserReadUsingObjectId"> <OutputClaims> <OutputClaim ClaimTypeReferenceId="city" /> <OutputClaim ClaimTypeReferenceId="usertype" /> </OutputClaims> </TechnicalProfile> <TechnicalProfile Id="AAD-UserWriteUsingAlternativeSecurityId"> <Metadata> <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item> </Metadata> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider> <ClaimsProvider> <DisplayName>SRAM UserInfo</DisplayName> <TechnicalProfiles> <TechnicalProfile Id="SRAM-OAUTH-UserInfo"> <DisplayName>SRAM OAuth2 UserInfo</DisplayName> <Protocol Name="OAuth2" /> <Metadata> <Item Key="authorization_endpoint">https://proxy.acc.sram.eduteams.org/saml2sp/OIDC/authorization</Item> <Item Key="response_types">code</Item> <Item Key="response_mode">query</Item> <Item Key="client_id">APP-1A1EE4A1-9FBD-4AB0-B926-0B4F96C381B6</Item> <Item Key="scope">openid profile email voperson_external_id</Item> <Item Key="AccessTokenEndpoint">https://proxy.acc.sram.eduteams.org/OIDC/token</Item> <Item Key="HttpBinding">POST</Item> <Item Key="token_endpoint_auth_method">client_secret_post</Item> <Item Key="ClaimsEndpoint">https://proxy.acc.sram.eduteams.org/OIDC/userinfo</Item> <Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item> </Metadata> <CryptographicKeys> <Key Id="client_secret" StorageReferenceId="B2C_1A_SRAMSecret" /> </CryptographicKeys> <OutputClaims> <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" /> <OutputClaim ClaimTypeReferenceId="givenName" /> <OutputClaim ClaimTypeReferenceId="surname" /> <OutputClaim ClaimTypeReferenceId="email" /> <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="sub"/> <OutputClaim ClaimTypeReferenceId="voPersonExternalID" /> <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="sram.surf.nl" AlwaysUseDefaultValue="true" /> <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="SRAMAuthentication" AlwaysUseDefaultValue="true" /> </OutputClaims> <OutputClaimsTransformations> <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" /> <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" /> <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" /> </OutputClaimsTransformations> </TechnicalProfile> </TechnicalProfiles> </ClaimsProvider> </ClaimsProviders>
Last thing we need to add is a set of Claims and (slightly adjusted) ClaimsTransformations. City and usertype are added to showcase immutable AAD sourced output claims:
<BuildingBlocks> <ClaimsSchema> <ClaimType Id="voPersonExternalID"> <DisplayName>voPersonExternalID</DisplayName> <DataType>stringCollection</DataType> <DefaultPartnerClaimTypes> <Protocol Name="OAuth2" PartnerClaimType="voperson_external_id" /> <Protocol Name="OpenIdConnect" PartnerClaimType="voperson_external_id" /> </DefaultPartnerClaimTypes> </ClaimType> <ClaimType Id="city"> <DisplayName>City</DisplayName> <DataType>string</DataType> <DefaultPartnerClaimTypes> <Protocol Name="OAuth2" PartnerClaimType="city" /> <Protocol Name="OpenIdConnect" PartnerClaimType="city" /> </DefaultPartnerClaimTypes> </ClaimType> <ClaimType Id="usertype"> <DisplayName>Type</DisplayName> <DataType>string</DataType> <DefaultPartnerClaimTypes> <Protocol Name="OAuth2" PartnerClaimType="user_type" /> <Protocol Name="OpenIdConnect" PartnerClaimType="user_type" /> </DefaultPartnerClaimTypes> </ClaimType> </ClaimsSchema> <ClaimsTransformations> <ClaimsTransformation Id="CreateUserPrincipalName" TransformationMethod="FormatStringClaim"> <InputParameters> <InputParameter Id="stringFormat" DataType="string" Value="sram_{0}@{RelyingPartyTenantId}" /> </InputParameters> </ClaimsTransformation> </ClaimsTransformations> </BuildingBlocks>
When we now register an app and define the return uri to https://jwt.ms, we can test the UserJourney by selecting Run now the B2C_1A_USERINFO policy:
After the Journey, we land that the https://jwt.ms claims test page: