Skip to content

Instantly share code, notes, and snippets.

@rbrayb
Created July 17, 2019 04:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbrayb/1025b45ff224875dac6bbb13970f1cdb to your computer and use it in GitHub Desktop.
Save rbrayb/1025b45ff224875dac6bbb13970f1cdb to your computer and use it in GitHub Desktop.
Using a JWT to invoke an Azure AD B2C flow using id_token_hint
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http.Extensions;
using AADB2C.Invite.Models;
using System.Net.Mail;
using System.Net;
namespace AADB2C.Invite.Controllers
{
public class HomeController : Controller
{
private static Lazy<X509SigningCredentials> SigningCredentials;
private readonly AppSettingsModel AppSettings;
private readonly IHostingEnvironment HostingEnvironment;
// Sample: Inject an instance of an AppSettingsModel class into the constructor of the consuming class,
// and let dependency injection handle the rest
public HomeController(IOptions<AppSettingsModel> appSettings, IHostingEnvironment hostingEnvironment)
{
this.AppSettings = appSettings.Value;
this.HostingEnvironment = hostingEnvironment;
// Sample: Load the certificate with a private key (must be pfx file)
SigningCredentials = new Lazy<X509SigningCredentials>(() =>
{
X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection = certStore.Certificates.Find(
X509FindType.FindByThumbprint,
this.AppSettings.SigningCertThumbprint,
false);
// Get the first cert with the thumb-print
if (certCollection.Count > 0)
{
return new X509SigningCredentials(certCollection[0]);
}
throw new Exception("Certificate not found");
});
}
[HttpGet]
public ActionResult Index(string Name, string email, string phone)
{
if (string.IsNullOrEmpty(email))
{
ViewData["Message"] = "";
return View();
}
string token = BuildIdToken(Name, email);
string link = BuildUrl(token);
string Body = string.Empty;
string htmlTemplate = System.IO.File.ReadAllText(Path.Combine(this.HostingEnvironment.ContentRootPath, "App_Data\\Template.html"));
try
{
//MailMessage mailMessage = new MailMessage();
//mailMessage.To.Add(email);
//mailMessage.From = new MailAddress(AppSettings.SMTPFromAddress);
//mailMessage.Subject = AppSettings.SMTPSubject;
//mailMessage.Body = string.Format(htmlTemplate, email, link);
//mailMessage.IsBodyHtml = true;
//SmtpClient smtpClient = new SmtpClient(AppSettings.SMTPServer, AppSettings.SMTPPort);
//smtpClient.Credentials = new NetworkCredential(AppSettings.SMTPUsername, AppSettings.SMTPPassword);
//smtpClient.EnableSsl = AppSettings.SMTPUseSSL;
//smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
//smtpClient.Send(mailMessage);
//ViewData["Message"] = $"Email sent to {email}";
ViewData["Message"] = $"Link is : {link}";
}
catch (Exception ex)
{
throw ex;
}
return View();
}
private string BuildIdToken(string Name, string ClubId)
{
//string issuer = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase.Value}/";
string issuer = "https://aadb2cinvite20190711090801.azurewebsites.net/";
// All parameters send to Azure AD B2C needs to be sent as claims
IList<System.Security.Claims.Claim> claims = new List<System.Security.Claims.Claim>();
claims.Add(new System.Security.Claims.Claim("name", Name, System.Security.Claims.ClaimValueTypes.String, issuer));
claims.Add(new System.Security.Claims.Claim("email", ClubId, System.Security.Claims.ClaimValueTypes.String, issuer));
// Create the token
JwtSecurityToken token = new JwtSecurityToken(
issuer,
this.AppSettings.B2CClientId,
claims,
DateTime.Now,
DateTime.Now.AddDays(7),
HomeController.SigningCredentials.Value);
// Get the representation of the signed token
JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
return jwtHandler.WriteToken(token);
}
private string BuildUrl(string token)
{
string nonce = Guid.NewGuid().ToString("n");
return string.Format(this.AppSettings.B2CSignUpUrl,
this.AppSettings.B2CTenant,
this.AppSettings.B2CPolicy,
this.AppSettings.B2CClientId,
Uri.EscapeDataString(this.AppSettings.B2CRedirectUri),
nonce) + "&id_token_hint=" + token;
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
<?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="tenant.onmicrosoft.com"
PolicyId="B2C_1A_signin_with_email"
PublicPolicyUri="http://tenant.onmicrosoft.com/B2C_1A_signin_with_email"
DeploymentMode="Development"
UserJourneyRecorderEndpoint="urn:journeyrecorder:applicationinsights" >
<BasePolicy>
<TenantId>tenant.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<!--Sample: Stores the error message for unsolicited request (a request without id_token_hint) and user not found-->
<ClaimType Id="errorMessage">
<DisplayName>Error</DisplayName>
<DataType>string</DataType>
<UserHelpText>Add help text here</UserHelpText>
<UserInputType>Paragraph</UserInputType>
</ClaimType>
</ClaimsSchema>
<ClaimsTransformations>
<!--Sample: Initiates the errorMessage claims type with the error message-->
<ClaimsTransformation Id="CreateUnsolicitedErrorMessage" TransformationMethod="CreateStringClaim">
<InputParameters>
<InputParameter Id="value" DataType="string" Value="You cannot sign-in without invitation. Please click the link in the email" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
</OutputClaims>
</ClaimsTransformation>
<!--Sample: Initiates the errorMessage claims type with the error message user not found-->
<ClaimsTransformation Id="CreateUserNotFoundErrorMessage" TransformationMethod="CreateStringClaim">
<InputParameters>
<InputParameter Id="value" DataType="string" Value="You aren't registered in the system! Please contact the Help Desk on 0800 xxx yyy and ask to be added to the system" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
</OutputClaims>
</ClaimsTransformation>
</ClaimsTransformations>
</BuildingBlocks>
<ClaimsProviders>
<!--Sample: This technical profile specifies how B2C should validate your token, and what claims you want B2C to extract from the token.
The METADATA value in the TechnicalProfile meta-data is required.
The “IdTokenAudience” and “issuer” arguments are optional (see later section)-->
<ClaimsProvider>
<DisplayName>My ID Token Hint ClaimsProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="IdTokenHint_ExtractClaims">
<DisplayName> My ID Token Hint TechnicalProfile</DisplayName>
<Protocol Name="None" />
<Metadata>
<!--Sample action required: replace with your endpoint location -->
<Item Key="METADATA">https://aadb2cinvitexxx.azurewebsites.net/.well-known/openid-configuration</Item>
<!-- <Item Key="IdTokenAudience">your_optional_audience_override</Item> -->
<!-- <Item Key="issuer">your_optional_token_issuer_override</Item> -->
</Metadata>
<OutputClaims>
<!--Sample: Read the email cliam from the id_token_hint-->
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Self Asserted</DisplayName>
<TechnicalProfiles>
<!-- Demo: Show error message-->
<TechnicalProfile Id="SelfAsserted-Error">
<DisplayName>Unsolicited error message</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
<!-- Sample: Remove the continue button-->
<Item Key="setting.showContinueButton">false</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="errorMessage"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage"/>
</OutputClaims>
</TechnicalProfile>
<!-- Demo: Show unsolicited error message-->
<TechnicalProfile Id="SelfAsserted-Unsolicited">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUnsolicitedErrorMessage" />
</InputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-Error" />
</TechnicalProfile>
<!-- Demo: Show user not found error message-->
<TechnicalProfile Id="SelfAsserted-UserNotFound">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUserNotFoundErrorMessage" />
</InputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-Error" />
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-Hint">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="accountEnabled" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="givenName"/>
<OutputClaim ClaimTypeReferenceId="surname"/>
</OutputClaims>
<!-- <OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertAccountEnabledIsTrue" />
</OutputClaimsTransformations> -->
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignUpOrSignInWithEmail">
<OrchestrationSteps>
<!--Sample: Read the input claims from the id_token_hint-->
<OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />
<!-- Sample: Check if user tries to run the policy without invitation -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Read the user properties from the directory-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingEmailAddress" TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-Hint"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Sample: Check whether the user not existed in the directory -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAssertedUserNotFound" TechnicalProfileReferenceId="SelfAsserted-UserNotFound" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Issue an access token-->
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>
</UserJourneys>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignInWithEmail" />
<UserJourneyBehaviors>
<JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="your key"
DeveloperMode="true" ClientEnabled="false"
ServerEnabled="true" TelemetryVersion="1.0.0" />
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<!--Sample: Set the input claims to be read from the id_token_hint-->
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment