Blogs

Sign-up and sign-in with Atlassian using Azure AD B2C

This article describes how to add authentication for an Atlassian account to an Azure AD B2C custom policy.

Architecture

The following diagram illustrates the authentication flow for an Atlassian account to an Azure AD B2C custom policy.

The authentication flow requires an Azure function that exchanges an authorization code for an access token, because an Atlassian client must present an authorization code to Atlassian’s token endpoint using the HTTP POST method and JSON serialization, which isn’t supported by Azure AD B2C.

For information about Atlassian and OAuth 2.0, see https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/.

Prerequisites

  1. If you don’t already have one, then you must create an Azure AD B2C tenant that is linked to your Azure subscription.
  2. Prepare your Azure AD B2C tenant by creating the token signing and encryption keys and creating the Identity Experience Framework applications.
  3. Download one of the starter packs for Azure AD B2C from Microsoft’s GitHub repository.

Create the Azure function

  1. Create a C# function containing the following code. This implements a GetAtlassianAccessToken function that exchanges an authorization code for an access token. Then publish this C# function to a function app.
using System.Net;
using System.Net.Http;
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 SignInWithAtlassian
{
    public static class GetAtlassianAccessToken
    {
        private static readonly HttpClient InnerClient = new HttpClient();

        [FunctionName("GetAtlassianAccessToken")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest request,
            ILogger logger)
        {
            if (request.Form == null)
            {
                logger.LogError("Form is missing");
                return new BadRequestResult();
            }

            if (!request.Form.TryGetValue("grant_type", out var grantType))
            {
                logger.LogError("The grant type is missing");
                return new BadRequestResult();
            }

            if (!request.Form.TryGetValue("client_id", out var clientId))
            {
                logger.LogError("The client identifier is missing");
                return new BadRequestResult();
            }

            if (!request.Form.TryGetValue("client_secret", out var clientSecret))
            {
                logger.LogError("The client secret is missing");
                return new BadRequestResult();
            }

            if (!request.Form.TryGetValue("code", out var code))
            {
                logger.LogError("Code is missing");
                return new BadRequestResult();
            }

            if (!request.Form.TryGetValue("redirect_uri", out var redirectUri))
            {
                logger.LogError("The redirection URI is missing");
                return new BadRequestResult();
            }

            var innerRequest = new HttpRequestMessage(HttpMethod.Post, "https://auth.atlassian.com/oauth/token");

            var innerRequestModel = new AtlassianAccessTokenRequestModel
            {
                GrantType = grantType,
                ClientId = clientId,
                ClientSecret = clientSecret,
                Code = code,
                RedirectUri = redirectUri
            };

            innerRequest.Content = new JsonContent(innerRequestModel);
            var innerResponse = await InnerClient.SendAsync(innerRequest);

            if (innerResponse.StatusCode != HttpStatusCode.OK && innerResponse.StatusCode != HttpStatusCode.Unauthorized && innerResponse.StatusCode != HttpStatusCode.Forbidden)
            {
                return new InternalServerErrorResult();
            }

            if (innerResponse.StatusCode == HttpStatusCode.Unauthorized)
            {
                var innerErrorResponseModel = await innerResponse.Content.ReadAsJsonAsync<AtlassianAccessTokenErrorResponseModel>();
                logger.LogError($"Error: {innerErrorResponseModel.Error}, error description: {innerErrorResponseModel.ErrorDescription}.");
                return new UnauthorizedResult();
            }

            if (innerResponse.StatusCode == HttpStatusCode.Forbidden)
            {
                var innerErrorResponseModel = await innerResponse.Content.ReadAsJsonAsync<AtlassianAccessTokenErrorResponseModel>();
                logger.LogError($"Error: {innerErrorResponseModel.Error}, error description: {innerErrorResponseModel.ErrorDescription}.");
                return new ForbiddenResult();
            }

            var innerResponseModel = await innerResponse.Content.ReadAsJsonAsync<AtlassianAccessTokenResponseModel>();

            var responseModel = new
            {
                access_token = innerResponseModel.AccessToken
            };

            return new OkObjectResult(innerResponseModel);
        }
    }
}

The model classes are implemented as follows.

using Newtonsoft.Json;

namespace SignInWithAtlassian
{
    public class AtlassianAccessTokenRequestModel
    {
        [JsonProperty("client_id")]
        public string ClientId { get; set; }

        [JsonProperty("client_secret")]
        public string ClientSecret { get; set; }

        [JsonProperty("code")]
        public string Code { get; set; }

        [JsonProperty("grant_type")]
        public string GrantType { get; set; }

        [JsonProperty("redirect_uri")]
        public string RedirectUri { get; set; }
    }

    public class AtlassianAccessTokenResponseModel
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    }

    public class AtlassianAccessTokenErrorResponseModel
    {
        [JsonProperty("error")]
        public string Error { get; set; }

        [JsonProperty("error_description")]
        public long ErrorDescription { get; set; }
    }
}

The JsonContent class is implemented as follows.

using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace SignInWithAtlassian
{
    public class JsonContent : StringContent
    {
        public JsonContent(object value)
            : base(JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json")
        {
        }
    }
}

The ForbiddenResult class is implemented as follows.

using Microsoft.AspNetCore.Mvc;

namespace SignInWithAtlassian
{
    public class ForbiddenResult : StatusCodeResult
    {
        public ForbiddenResult()
            : base(403)
        {
        }
    }
}

The ReadAsJsonAsync extension method is implemented as follows.

using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace SignInWithAtlassian
{
    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 an Atlassian application

  1. Sign in to Atlassian’s developer console.
  2. In the My Apps page, select Create and then select OAuth 2.0 integration.
  3. In the Create a new OAuth 2.0 (3LO) integration page, enter an application name, accept Atlassian’s developer terms, and then select Create.
  4. Select Distribution.
  5. In the Distribution page, in the Distribution controls section, select Edit.
  6. In the Edit distribution controls section:
    1. In the Distribution status section, select Sharing.
    2. In the Vendor & security details section, enter the required fields.
    3. In the Personal data declaration section, select Yes for the Does your app store personal data? field.
    4. Select Save changes.
  7. Select Permissions.
  8. In the Permissions page, select Add for the User identity API list item.
  9. Select Authorization.
  10. In the Authorization page, select Configure for the OAuth 2.0 (3LO) list item.
  11. In the OAuth 2.0 authorization code grants (3LO) for apps section, enter either https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/oauth2/authresp if you use the built-in domain or https://your-domain-name/your-tenant-name.onmicrosoft.com/oauth2/authresp if you use a custom domain for the Callback URL field. Replace your-tenant-name with your tenant name and your-domain-name with your custom domain. Then, select Save changes.
  12. Select Settings.
  13. In the Settings page, in the Authentication details section, copy the Client ID and Secret fields.

Add the client secret for the Atlassian application as a policy key

  1. Sign in to the Azure AD B2C portal.
  2. Select Identity Experience Framework.
  3. Select Policy keys.
  4. Select Add.
  5. In the Create a key section, enter the following fields and then select Create:
    1. Options: Manual
    2. Name: AtlassianClientSecret
    3. Secret: Paste the Secret field that was copied in the previous section.

Configure Atlassian as an identity provider

  1. Open the TrustFrameworkExtensions.xml file.
  2. Find the ClaimsProviders element. If it doesn’t exist, then add it to the TrustFrameworkPolicy element.
  3. Add the following ClaimsProvider element to the ClaimsProviders element. Replace your-atlassian-client-id with the Client ID field that was copied in the Configure an Atlassian application section. Replace your-function-app-name with the function app name that was created in the Create the Azure function section.
<ClaimsProvider>
  <Domain>atlassian.com</Domain>
  <DisplayName>Atlassian</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="Atlassian-OAuth2">
      <DisplayName>Atlassian</DisplayName>
      <Protocol Name="OAuth2" />
      <Metadata>
        <Item Key="authorization_endpoint">https://auth.atlassian.com/authorize?audience=api.atlassian.com&amp;scope=read:me</Item>
        <Item Key="client_id">your-atlassian-client-id</Item>
        <Item Key="HttpBinding">POST</Item>
        <Item Key="AccessTokenEndpoint">https://your-function-app-name.azurewebsites.net/api/GetAtlassianAccessToken</Item>
        <Item Key="ClaimsEndpoint">https://api.atlassian.com/me</Item>
        <Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item>
        <Item Key="UsePolicyInRedirectUri">false</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="client_secret" StorageReferenceId="B2C_1A_AtlassianClientSecret" />
      </CryptographicKeys>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="atlassian.com" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="account_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

  1. Open the TrustFrameworkBase.xml file.
  2. Copy the UserJourney element that includes Id="SignUpOrSignIn".
  3. Open the TrustFrameworkExtensions.xml file and find the UserJourneys element. If it doesn’t exist, then add it to the TrustFrameworkPolicy element.
  4. 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 "AtlassianSignUpOrSignIn".

Add the identity provider to the user journey

  1. Add the claims provider that was configured in the Configure Atlassian 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="AtlassianExchange" />
  </ClaimsProviderSelections>
  ...
</OrchestrationStep>

<OrchestrationStep Order="2" Type="ClaimsExchange">
  ...
  <ClaimsExchanges>
    <ClaimsExchange Id="AtlassianExchange" TechnicalProfileReferenceId="Atlassian-OAuth2" />
  </ClaimsExchanges>
  ...
</OrchestrationStep>

Configure the relying party policy

  1. Open the SignUpOrSignIn.xml file.
  2. Replace the ReferenceId attribute for the DefaultUserJourney element from "SignUpOrSignIn" to "AtlassianSignUpOrSignIn".
<RelyingParty>
  <DefaultUserJourney ReferenceId="AtlassianSignUpSignIn" />
  ...
</RelyingParty>

Upload and test the custom policy

  1. Upload all policy files in the following order to your Azure AD B2C tenant:
    1. TrustFrameworkBase.xml
    2. TrustFrameworkLocalization.xml
    3. TrustFrameworkExtensions.xml
    4. SignUpOrSignIn.xml
  2. Test the B2C_1A_signup_signin policy from your Azure AD B2C tenant.
[mailpoet_form id="1"]

Other Recent Blogs

Multiple conditions for Bicep resource deployments

After working with Bicep templates we have discovered a useful mechanism, multiple conditions. Multiple conditions provide ways to be more specific in the way we perform conditional deployments and enable us to have more control on the conditions we match on.

Read More »

Arinco trades as Arinco (VIC) Pty Ltd
and Arinco (NSW) Pty Ltd

All Rights Reserved

 

Level 9, 360 Collins Street, 
Melbourne VIC 3000

Level 2, 24 Campbell St,
Sydney NSW 2000

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.