AAD B2C Conditional MFA - Don't Enroll - azure-ad-b2c

We use AAD B2C for 30k users and a range of apps. I want to progressively rollout to MFA to a subset of users. Conditional Access Policy sounds perfect, except that as part of the build-in sign-in user flow, users are prompted to enroll in MFA and enter email/phone, even if they are not covered by the Conditional Access policy. This defaults the purpose of slowly rolling out MFA.
This pages suggests Enrollment occurs regardless to Conditional Access.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/conditional-access-user-flow?pivots=b2c-user-flow
I've found ways to update the user's authentication phone or email via the Graph API... but I'd rather not do this since I'm not certain I have the correct phone number on hand.
Is there a way to delay the enrollment process?

You could do this with a custom policy.
Flag the users you’d like to enrol with an extension attribute. Use graph api to do this.
In the custom policy read this attribute after the user verified their credentials. Also read the phone number attribute.
$tenant = "contoso.onmicrosoft.com"
#B2CUserMigration Application Registration Application Id
$ClientID = ""
#B2CUserMigration Application Registration generated key (client secret)
$ClientSecret = ""
$loginURL = "https://login.microsoftonline.com"
$resource = "https://graph.microsoft.com"
# Get an OAuth 2 access token based on client id, secret and tenant
$body = #{grant_type="client_credentials";client_id=$ClientID;client_secret=$ClientSecret;resource=$resource}
$oauth = Invoke-RestMethod -Method Post -Uri $loginURL/$tenant/oauth2/token?api-version=1.0 -Body $body
#Part 2 - Register the extension attribute named "requiresMigration" into Azure AD B2C
#ObjectID of the b2c-extensions-app App Registration
$AppObjectID = ""
#Set the endpoint to register extension attributes
$url = "$resource/v1.0/applications/$AppObjectID/extensionProperties"
#Define the extension attribute
$body = #"
{
"name": "requiresMFA",
"dataType": "Boolean",
"targetObjects": ["User"]
}
"#
#Patch the user
$objectId = "user objectId to update"
$url = "$resource/v1.0/users/$objectId"
$body = #"
{
extension_GUID-WITHOUT-DASHES_requiresMFA: true
}
"#
If the attribute is set to true, and the phone number null, then enrol into mfa. Control the mfa orchestration step with a set of preconditions.
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="requiresMFA" DefaultValue="false"/>
<OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" DefaultValue="00"/>
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<OrchestrationStep Order="XX" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>isActiveMFASession</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>requiresMFA</Value>
<Value>False</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
</ClaimsExchanges>
</OrchestrationStep>

Related

"JWT Validation Failed: JWT not present.." in Azure API Management Service

For reference I am attempting to reproduce the solution talked about here: https://www.tech-findings.com/2020/02/securing-logic-app-with-azure-active-directory.html to use API Management to secure an Azure Logic App.
I am getting a JWT Error. When I visit the app url in the browser it gives:
{ "statusCode": 404, "message": "Resource not found" }
In the API Management Service test I get:
HTTP/1.1 401 Unauthorized
Following the trace through it shows:
validate-jwt (-0.111 ms)
{
"message": "JWT Validation Failed: JWT not present.."
}
I did some googling and tried the solutions at:
JWT validation failure error in azure apim
and
https://learn.microsoft.com/en-us/answers/questions/108008/azure-apim-jwt-token-validation-policy.html
Here is the inbound policy of from the API Management design:
<policies>
<inbound>
<base />
<set-method id="apim-generated-policy">POST</set-method>
<rewrite-uri id="apim-generated-policy" template="/request/paths/invoke//?api-version=2016-06-01&sp=/triggers/request/run&sv=1.0&sig={{[[LOGIC APP NAME]]_request-invoke_XXXXXXXXXXXXXXXXXXXXXXXX}}" />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Request is not authorized or token failed" require-expiration-time="false" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.windows.net/[[TENANT NAME]].onmicrosoft.com/.well-known/openid-configuration" />
<audiences>
<audience>[[THE ID OF A REGISTERED APP]]</audience>
</audiences>
</validate-jwt>
<set-header name="Authorization" exists-action="delete" />
<set-header name="apim-generated-policy" exists-action="delete" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
This is the manifest of the registered app:
{
"id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"acceptMappedClaims": null,
"accessTokenAcceptedVersion": 2,
"addIns": [],
"allowPublicClient": null,
"appId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"appRoles": [],
"oauth2AllowUrlPathMatching": false,
"createdDateTime": "2020-12-22T19:48:36Z",
"disabledByMicrosoftStatus": null,
"groupMembershipClaims": null,
"identifierUris": [
"api://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
],
"informationalUrls": {
"termsOfService": null,
"support": null,
"privacy": null,
"marketing": null
},
"keyCredentials": [],
"knownClientApplications": [],
"logoUrl": null,
"logoutUrl": null,
"name": "LabsTestApp",
"oauth2AllowIdTokenImplicitFlow": false,
"oauth2AllowImplicitFlow": false,
"oauth2Permissions": [],
"oauth2RequirePostResponse": false,
"optionalClaims": null,
"orgRestrictions": [],
"parentalControlSettings": {
"countriesBlockedForMinors": [],
"legalAgeGroupRule": "Allow"
},
"passwordCredentials": [],
"preAuthorizedApplications": [],
"publisherDomain": "[[TENANT NAME]].onmicrosoft.com",
"replyUrlsWithType": [],
"requiredResourceAccess": [],
"samlMetadataUrl": null,
"signInUrl": null,
"signInAudience": "AzureADandPersonalMicrosoftAccount",
"tags": [],
"tokenEncryptionKeyId": null
}
Hoping you can help out - point me in the right direction.
For this question, there are more than one problem in your steps.
1. You mentioned the error { "statusCode": 404, "message": "Resource not found" } when you request the url in browser. The reason is when you request it in browser, it request with Get method but the url should be request with Post method. So it shows 404 not found.
2. When you test in API Management service, it shows 401 Unauthorized. The reason for this error is you did not provide the access token or the access token you provided is invalid. The steps in the document you mentioned are incomplete, please refer to the steps below:
1). First please make sure you have completed all of the steps in the document you provided.
2). Then go to the app you registered in azure ad and click "Manifest" tab, add a appRole in the json of "Manifest".
You can specify a name(anything you want) for this role, I named the role as Writer as the screenshot above shows. And you can also specify a "id"(in GUID format) as the value of the id field in appRole. For more details of add appRole, you can refer to this document.
3). You need to register another app in azure ad as the client app. Do same register operation as your document shows to register the other app, I registered the app and named huryGetToken4. Go to this app and click "API permissions" tab, click "Add a permission" and find the original app you registered, then add the permission Writer.
After add the Writer permission, you also need to click "Grant admin consent for xxx".
Then click "Certificates & secrets" tab, click "New client secret" to generate a client secret. Copy this secret because it will just show one time.
4). Then you need to get access token, please refer to the screenshot below to request for access token.
In the screenshot above, you need to replace the <tenant id> with your tenant id in the host url. And you also need to input the first three parameters. The last parameter grant_type is static.
5). Request for the access token, you will get the response like below screenshot.
Copy the value of access_token and paste it to this page to decode the token, you can see the claim roles with Writer permission in it. This claim is what you need to check in the <validate-jwt> policy in your APIM.
6). Go to your apim and click the pencil icon of validate-jwt policy.
7). Edit the "Reauired claims" like screenshot below:
8). After that, you can test the api in APIM service. Add a header with key: Authorization, value: Bearer <your access token>(note there is a blank between Bearer and access token).

AAD B2C: Output a nested JSON object in a JWT for Hasura

We are using Hasura to provide our GraphQL API to consumers. Currently we use Auth0 to authenticate users, but we would like to migrate to Azure AD B2C.
A requirement of JWT security with Hasura is using the "https://hasura.io/jwt/claims" namespace to provide custom claims (such as X-Hasura-Org-Id, X-Hasura-App-Id, etc).
I have been able to get AAD B2C to:
Gather the required values for these custom claims using a REST API;
Transform the individual string / stringCollection values into a JSON object using a ClaimsTransformation; and
Return the transformed claims in the JWT.
However, I cannot figure out how to get the JSON object to appear in the final JWT without the contents being escaped - i.e. being output as a string rather than an object.
Is AAD B2C capable of outputting nested objects in a JWT?
What we're hoping to achieve
This is what Hasura wants the JWT namespace to look like (note the https://hasura.io/jwt/claims object)
{
"exp": 1588405829,
"nbf": 1588402229,
"ver": "1.0",
"iss": "https://<redacted>.b2clogin.com/<redacted>/v2.0/",
"sub": "<redacted>",
"aud": "<redacted>",
"acr": "b2c_1a_aaa_signupsignin",
"nonce": "defaultNonce",
"iat": 1588402229,
"auth_time": 1588402229,
"given_name": "Test",
"family_name": "User",
"name": "Test User",
"email": "test#example.com",
"idp": "facebook.com",
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles":["role1","role2","role3"],
"x-hasura-default-role":"role1",
"x-hasura-org-id":"test-org",
"x-hasura-user-id":"test-user-id",
"x-hasura-app-id":"<redacted>"
}
}
What we're getting at the moment
Here's an example of the JWT from AAD B2C:
{
"exp": 1588405829,
"nbf": 1588402229,
"ver": "1.0",
"iss": "https://<redacted>.b2clogin.com/<redacted>/v2.0/",
"sub": "<redacted>",
"aud": "<redacted>",
"acr": "b2c_1a_aaa_signupsignin",
"nonce": "defaultNonce",
"iat": 1588402229,
"auth_time": 1588402229,
"given_name": "Test",
"family_name": "User",
"name": "Test User",
"email": "test#example.com",
"idp": "facebook.com",
"https://hasura.io/jwt/claims": "{\"x-hasura-allowed-roles\":[\"role1\",\"role2\",\"role3\"],\"x-hasura-default-role\":\"role1\",\"x-hasura-org-id\":\"test-org\",\"x-hasura-user-id\":\"test-user-id\",\"x-hasura-app-id\":\"<redacted>\"}"
}
There doesn't appear to be an option to store a claim as an object, only a string.
How we got there
An example of the ClaimsTransformation:
<ClaimsTransformation Id="hasuraClaimsToJson" TransformationMethod="GenerateJson">
<InputClaims>
<InputClaim ClaimTypeReferenceId="x-hasura-allowed-roles" TransformationClaimType="x-hasura-allowed-roles" />
<InputClaim ClaimTypeReferenceId="x-hasura-default-role" TransformationClaimType="x-hasura-default-role" />
<InputClaim ClaimTypeReferenceId="x-hasura-org-id" TransformationClaimType="x-hasura-org-id" />
<InputClaim ClaimTypeReferenceId="x-hasura-user-id" TransformationClaimType="x-hasura-user-id" />
</InputClaims>
<InputParameters>
<InputParameter Id="x-hasura-app-id" DataType="string" Value="internal-redacted-uuid" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="hasuraClaims" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
Example RelyingParty config:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="hasuraClaims" PartnerClaimType="https://hasura.io/jwt/claims" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
Have you considered passing claims_format in HASURA_GRAPHQL_JWT_SECRET with stringified_json, so that Hasura can accept claims as string instead of object. I found the documentation entry on it here: https://hasura.io/docs/1.0/graphql/manual/auth/authentication/jwt.html
Can you share the desired/expected output of JSON that you want from B2C? If you review the doc: https://learn.microsoft.com/en-us/azure/active-directory-b2c/json-transformations, B2C is capable of returning a complex JSON.
Sample JSON Output by GenerateJSON:
"personalizations": [
{
"to": [
{
"email": "someone#example.com"
}
],
"dynamic_template_data": {
"otp": "346349",
"verify-email" : "someone#example.com"
},
"subject": "Contoso account email verification code"
}
],
"template_id": "d-989077fbba9746e89f3f6411f596fb96",
"from": {
"email": "service#contoso.com"
}
}
For those interested, it turns out that this is not currently possible with Azure AD B2C.
Instead, we switched from using internal JWT auth in Hasura to using webhooks, and validated the B2C token using a small Node.js Function App.
EDIT:
As per the answer above, Hasura has a workaround using the claims_format parameter. This means that you can potentially use AAD B2C for Hasura authentication without the need for implementing Webhooks.

Policy rewrite-uri To Append Context Variable in Azure APIM

What is the approach to a simple append to the url of a context variable such as context.Variables["accountKey"] during a policy rewrite?
The end result should be /accounts/232.
I have success earlier in setting it
set-variable (0.003 ms)
{
"message": "Context variable was successfully set.",
"name": "accountKey",
"value": "232"
}
One of the things tried:
<policies>
<inbound>
<base />
<rewrite-uri template="/accounts/{accountKey}" />
</inbound>
But I get this error
> Error Receive
> rewrite-uri (0.260 ms) {
> "messages": [
> null,
> "Variable accountKey has no value.",
> "Variable accountKey has no value."
> ] }
Configure the inbound rule in the policy as follows:
<inbound>
<base />
<set-variable name="accountKey" value="232" />
<rewrite-uri template="#{
return "/account/" + context.Variables.GetValueOrDefault<string>("accountKey");
}"/>
</inbound>
{} in rewrite-uri are for query string parameters in the original request URL.
Find more details about rewrite-uri section in the Microsoft docs at Rewrite URL - API Management transformation policies.
I ended up using this call as shown as an alternative:
<rewrite-uri template="#($"/account/{(string)context.Variables["accountKey"]}/")" />
In my case, my URL was
https://test.com/send/
I need to add query string from context variable name (name = myName)
https://test.com/send/myName
This is working for me:
<rewrite-uri template="#($"{(string)context.Variables["name"]}")" />

Validating "scp" in Api Management's validate-jwt

I am attempting to validate that a passed in JWT token has the scopes "labresults.read" and "user_impersonation". I did the following policy snippet
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Invalid JWT Token" require-signed-tokens="true">
<openid-config url="(snip)" />
<audiences>
<audience>(snip)</audience>
</audiences>
<required-claims>
<claim name="scp" match="all">
<value>labresults.read</value>
<value>user_impersonation</value>
</claim>
</required-claims>
</validate-jwt>
I pass in a token that looks like
{
"iss": "(snip)",
"exp": 1522334650,
"nbf": 1522331050,
"aud": "(snip)",
"sub": "(snip)",
"email": "(snip)",
"name": "Scott Chamberlain",
"scp": "labresults.read user_impersonation",
"azp": "(snip)",
"ver": "1.0",
"iat": 1522331050
}
When I do a "Try It" from the developer portal looking at the tracing the on-error reports
validate-jwt (648 ms){
"message": "JWT Validation Failed: Claim value mismatch: scp=labresults.read.."
}
Am I forced to use a single claim of
<claim name="scp">
<value>labresults.read user_impersonation</value>
</claim>
I really would not like to as I do not want to force on the consumers of this api that those two scopes will be the only things passed in and in that specific order.
What do I need to do to validate the scope the propper way?
Got an answer from someone at Microsoft on the MSDN Forums.
I would suggest you to specify “separator” attribute in policy
statement to validate the JWT token and see if it helps. For more
information, you might refer this document:
https://learn.microsoft.com/en-us/azure/api-management/api-management-access-restriction-policies#ValidateJWT.
Swikruti BoseMicrosoft-MSFT (MSFT CSG)
I totally overlooked the seperator attribute when i looked at the documentation the first time.

Configure a cloudservice for autentication with Azure Active Directory

I've got a single tenant cloudservice that I want to be accessible to my company's employees only. The solution has a web role and a worker role.
Web.Config
<add key="ida:Tenant" value="MyCompany.onmicrosoft.com" />
<add key="ida:Audience" value="https://MyCompany.onmicrosoft.com/MySolutionWebRole" />
<add key="ida:ClientID" value="44421xxx-xxxx-xxxx-xxxx-xxxxxxx7024" />
<add key="ida:Password" value="i6fMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4Yk=" />
<add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
<add key="ida:PostLogoutRedirectUri" value="https://localhost:44322/" />
Also, I got the same settings in Cloud.config:
<Setting name="ida.Tenant" value="MyCompany.onmicrosoft.com" />
<Setting name="ida.Audience" value="https://MyCompany.onmicrosoft.com/MySolutionWebRole" />
<Setting name="ida.ClientID" vvalue="44421xxx-xxxx-xxxx-xxxx-xxxxxxx7024" />
<Setting name="ida.Password" value="i6fMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4Yk=" />
<Setting name="ida.AADInstance" value="https://login.microsoftonline.com/{0}" />
<Setting name="ida.PostLogoutRedirectUri" value="https://localhost:44322/" />
Moving on to Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
}
Finally, I've got the [Authorize] tag in my controller set up.
In the Azure Active Directory setup, I've got my cloudservice registered.
Application type is Web app / API, and Multi-tenanted is "No". Logout url is set to https://localhost:44322/Account/EndSession. I have not changed or edited the Manifest.
When I try to enter the cloud service, I'm redirected to my organization login page (all well so far), but after entering password I'm greeted my an error message.
We have problems loggin you in. We received an illegal request. (freely translated)
Correlation ID: 21f4089f-1952-4f57-aead-173a66c1408d Timestamp:
2016-09-26 10:24:14Z AADSTS90093: This application requires
application permissions to another application. Consent for
application permissions can only be performed by an administrator.
Sign out and sign in as an administrator or contact one of your
organization's administrators.
The url for the login request is as follows (the sceen where I enter my password);
https://login.microsoftonline.com/ fd2xxxxx-xxxx-xxxx-xxxxxxxf3f2/
oauth2/authorize?client_id=444xxxxx-xxxx-xxxx-xxxxxxxx024
&redirect_uri=https%3a%2f%2flocalhost%3a44322%2f
&response_mode=form_post &response_type=code+id_token
&scope=openid+profile&state=OpenIdConnect.AuthenticationProperties
%3dYkxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I have been looking at two example solutions based on web apps found at
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect and
https://github.com/Azure-Samples/active-directory-dotnet-webapp-multitenant-openidconnect
I'd be really grateful for any help on this matter
Turnes out I have to edit my manifest in Azure Active Directory App registration:
"requiredResourceAccess": [
{
"resourceAppId": "00000002-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "311a71xxxx-xxxx-xxxx-xxxx7156d8e6",
"type": "Scope"
},
{
"id": "5778995axxxx-xxxx-xxx-xxxx63a9f3f4d04",
"type": "Role"
}
When I removed the last entry (role, probably the worker role), I got a screen prompting me if I wanted to grant the the application reading rights for my Azure AD profile. After answering OK i was forwarded to localhost:44322 with a 404. The solution to that was to remove the postLogoutRedirectUri key from the configuration files, as well as to remove the two lines in Startup.Auth.cs
//PostLogoutRedirectUri = postLogoutRedirectUri,
//RedirectUri = postLogoutRedirectUri,
Now it's working as intended :)

Resources