This article describes how to add authentication for a Bitbucket account to an Azure AD B2C custom policy.
Architecture
The following diagram illustrates the authentication flow for a Bitbucket account to an Azure AD B2C custom policy.
The authentication flow requires an Azure function that retrieves claims for the authenticated user.
For information about Bitbucket and OAuth 2.0, see https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/.
Prerequisites
- If you don’t already have one, then you must create an Azure AD B2C tenant that is linked to your Azure subscription.
- Prepare your Azure AD B2C tenant by creating the token signing and encryption keys and creating the Identity Experience Framework applications.
- Download one of the starter packs for Azure AD B2C from Microsoft’s GitHub repository.
Create the Azure function
- Create a C# function containing the following code. This implements a GetBitbucketClaims function that retrieves claims for the authenticated user. Firstly, this function retrieves the ID and name claims for this user (see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get). Then, it retrieves the e-mail addresses for them (see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-emails-get) and returns the confirmed, primary e-mail address. Publish this C# function to a function app.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
namespace SignInWithBitbucket
{
public static class GetBitbucketClaims
{
private static readonly HttpClient InnerClient = new HttpClient();
[FunctionName("GetBitbucketClaims")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest request,
ILogger logger)
{
if (!request.Headers.TryGetValue("Authorization", out var authorizationHeaderValues))
{
logger.LogError("The Authorization header is missing");
return new BadRequestResult();
}
if (authorizationHeaderValues.Count > 1)
{
logger.LogError("The Authorization header is invalid");
return new BadRequestResult();
}
var authorizationHeaderValue = authorizationHeaderValues[0];
var authorizationHeaderValueParts = authorizationHeaderValue.Split(' ');
if (authorizationHeaderValueParts.Length != 2 || !authorizationHeaderValueParts[0].Equals("Bearer", StringComparison.OrdinalIgnoreCase))
{
logger.LogError("The Authorization header is invalid");
return new BadRequestResult();
}
InnerClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorizationHeaderValueParts[0], authorizationHeaderValueParts[1]);
// Get the current user.
// For more information, see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get.
var innerUserRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.bitbucket.org/2.0/user");
var innerUserResponse = await InnerClient.SendAsync(innerUserRequest);
if (innerUserResponse.StatusCode != HttpStatusCode.OK)
{
return new InternalServerErrorResult();
}
var innerUserResponseModel = await innerUserResponse.Content.ReadAsJsonAsync<BitbucketUserClaimsResponseModel>();
// List the e-mail addresses for the current user.
// For more information, see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-emails-get.
var innerUserEmailsRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.bitbucket.org/2.0/user/emails");
var innerUserEmailsResponse = await InnerClient.SendAsync(innerUserEmailsRequest);
if (innerUserResponse.StatusCode != HttpStatusCode.OK)
{
return new InternalServerErrorResult();
}
var innerUserEmailsResponseModel = await innerUserEmailsResponse.Content.ReadAsJsonAsync<BitbucketUserEmailsClaimsResponseModel>();
var responseModel = new
{
id = innerUserResponseModel.Uuid,
email = innerUserEmailsResponseModel.Values != null && innerUserEmailsResponseModel.Values.Any()
? innerUserEmailsResponseModel.Values.FirstOrDefault(userEmailModel => userEmailModel.IsPrimary && userEmailModel.IsConfirmed && userEmailModel.Type.Equals("email", StringComparison.OrdinalIgnoreCase))?.Email
: null,
name = innerUserResponseModel.DisplayName
};
return new OkObjectResult(responseModel);
}
}
}
The model classes are implemented as follows.
using Newtonsoft.Json;
namespace SignInWithBitbucket
{
public class BitbucketUserClaimsResponseModel
{
[JsonProperty("uuid")]
public string Uuid { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
}
public class BitbucketUserEmailsClaimsResponseModel
{
[JsonProperty("values")]
public IEnumerable<BitbucketUserEmailModel> Values { get; set; }
}
public class BitbucketUserEmailModel
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("is_confirmed")]
public bool IsConfirmed { get; set; }
[JsonProperty("is_primary")]
public bool IsPrimary { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
}
}
The ReadAsJsonAsync extension method is implemented as follows.
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace SignInWithBitbucket
{
public static class HttpContentExtensions
{
public static async Task<T> ReadAsJsonAsync<T>(this HttpContent content)
{
if (content == null)
{
return default;
}
var value = await content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(value);
}
}
}
Configure a Bitbucket application
- Log in to https://bitbucket.org/dashboard/overview.
- Select Your profile and settings and then select All workspaces.
- On the Workspaces page, select a workspace.
- On the workspace page, select Settings and then select OAuth consumers.
- On the OAuth consumers page, select Add consumer.
- On the Add OAuth consumer page, enter the following fields and then select Save:
- Name
- Callback URL: Enter either
https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/oauth2/authresp
if you use the built-in domain orhttps://your-domain-name/your-tenant-name.onmicrosoft.com/oauth2/authresp
if you use a custom domain for the Callback URL field. Replaceyour-tenant-name
with your tenant name andyour-domain-name
with your custom domain. - Permissions: In the Account section, select Email and Read.
- On the OAuth consumers page, expand the application name and then copy the Key and Secret fields.
Add the client secret for the Bitbucket application as a policy key
- Sign in to the Azure AD B2C portal.
- Select Identity Experience Framework.
- Select Policy keys.
- Select Add.
- In the Create a key section, enter the following fields and then select Create:
- Options:
Manual
- Name:
BitbucketClientSecret
- Secret: Paste the Secret field that was copied in the previous section.
- Options:
Configure Bitbucket as an identity provider
- Open the TrustFrameworkExtensions.xml file.
- Find the ClaimsProviders element. If it doesn’t exist, then add it to the TrustFrameworkPolicy element.
- Add the following ClaimsProvider element to the ClaimsProviders element. Replace
your-bitbucket-client-id
with the Key field that was copied in the Configure a Bitbucket application section. Replaceyour-function-app-name
with the function app name that was created in the Create the Azure function section.
<ClaimsProvider>
<Domain>bitbucket.org</Domain>
<DisplayName>Bitbucket</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Bitbucket-OAuth2">
<DisplayName>Bitbucket</DisplayName>
<Protocol Name="OAuth2" />
<Metadata>
<Item Key="client_id">your-bitbucket-client-id</Item>
<Item Key="authorization_endpoint">https://bitbucket.org/site/oauth2/authorize</Item>
<Item Key="AccessTokenEndpoint">https://bitbucket.org/site/oauth2/access_token</Item>
<Item Key="ClaimsEndpoint">https://your-function-app-name.azurewebsites.net/api/GetBitbucketClaims</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_BitbucketClientSecret" />
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="bitbucket.org" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="id" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Add a user journey
- Open the TrustFrameworkBase.xml file.
- Copy the UserJourney element that includes
Id="SignUpOrSignIn"
. - Open the TrustFrameworkExtensions.xml file and find the UserJourneys element. If it doesn’t exist, then add it to the TrustFrameworkPolicy element.
- Paste the UserJourney element that was copied in step 2 to the UserJourneys element and replace the Id attribute for this UserJourney element from
"SignUpOrSignIn"
to"BitbucketSignUpOrSignIn"
.
Add the identity provider to the user journey
- Add the claims provider that was configured in the Configure Bitbucket as an identity provider section to the user journey that was added in the previous section.
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
...
<ClaimsProviderSelection TargetClaimsExchangeId="BitbucketExchange" />
</ClaimsProviderSelections>
...
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
...
<ClaimsExchanges>
<ClaimsExchange Id="BitbucketExchange" TechnicalProfileReferenceId="Bitbucket-OAuth2" />
</ClaimsExchanges>
...
</OrchestrationStep>
Configure the relying party policy
- Open the SignUpOrSignIn.xml file.
- Replace the ReferenceId attribute for the DefaultUserJourney element from
"SignUpOrSignIn"
to"BitbucketSignUpOrSignIn"
.
<RelyingParty>
<DefaultUserJourney ReferenceId="BitbucketSignUpSignIn" />
...
</RelyingParty>
Upload and test the custom policy
- Upload all policy files in the following order to your Azure AD B2C tenant:
- TrustFrameworkBase.xml
- TrustFrameworkLocalization.xml
- TrustFrameworkExtensions.xml
- SignUpOrSignIn.xml
- Test the B2C_1A_signup_signin policy from your Azure AD B2C tenant.