This article describes how to add authentication for a Basecamp account to an Azure AD B2C custom policy.
Architecture
The following diagram illustrates the authentication flow for a Basecamp account to an Azure AD B2C custom policy.

The authentication flow requires two Azure functions:
- One that exchanges an authorization code for an access token, because Basecamp’s token endpoint contains the non-standard request parameter 
type=web_server, which isn’t supported by Azure AD B2C. - Another that retrieves flattened claims for the authenticated user
 
For information about Basecamp and OAuth 2.0, see https://github.com/basecamp/api/blob/master/sections/authentication.md.
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 Azure functions
- Create a C# function containing the following code. This implements a GetBasecampAccessToken function that exchanges an authorization code for an access token. Then publish this C# function to a function app.
 
using System.Collections.Generic;
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 SignInWithBasecamp
{
    public static class GetBasecampAccessToken
    {
        private static readonly HttpClient InnerClient = new HttpClient();
        [FunctionName("GetBasecampAccessToken")]
        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("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://launchpad.37signals.com/authorization/token");
            var innerRequestModel = new Dictionary<string, string>
            {
                {"type", "web_server"},
                {"client_id", clientId},
                {"client_secret", clientSecret},
                {"code", code},
                {"redirect_uri", redirectUri}
            };
            innerRequest.Content = new FormUrlEncodedContent(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<BasecampAccessTokenErrorResponseModel>();
                logger.LogError($"Error: {innerErrorResponseModel.Error}, error description: {innerErrorResponseModel.ErrorDescription}.");
                return new UnauthorizedResult();
            }
            if (innerResponse.StatusCode == HttpStatusCode.Forbidden)
            {
                var innerErrorResponseModel = await innerResponse.Content.ReadAsJsonAsync<BasecampAccessTokenErrorResponseModel>();
                logger.LogError($"Error: {innerErrorResponseModel.Error}, error description: {innerErrorResponseModel.ErrorDescription}.");
                return new ForbiddenResult();
            }
            var innerResponseModel = await innerResponse.Content.ReadAsJsonAsync<BasecampAccessTokenResponseModel>();
            var responseModel = new
            {
                access_token = innerResponseModel.AccessToken
            };
            return new OkObjectResult(responseModel);
        }
    }
}
- Create another C# function containing the following code. This implements a GetBasecampClaims function that retrieves flattened claims for the authenticated user. Then publish this C# function to a function app.
 
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 SignInWithBasecamp
{
    public static class GetBasecampClaims
    {
        private static readonly HttpClient InnerClient = new HttpClient();
        [FunctionName("GetBasecampClaims")]
        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();
            }
            var innerRequest = new HttpRequestMessage(HttpMethod.Get, "https://launchpad.37signals.com/authorization.json");
            innerRequest.Headers.Authorization = new AuthenticationHeaderValue(authorizationHeaderValueParts[0], authorizationHeaderValueParts[1]);
            var innerResponse = await InnerClient.SendAsync(innerRequest);
            if (innerResponse.StatusCode != HttpStatusCode.OK)
            {
                return new InternalServerErrorResult();
            }
            var innerResponseModel = await innerResponse.Content.ReadAsJsonAsync<BasecampClaimsResponseModel>();
            var responseModel = new
            {
                id = innerResponseModel.Identity.Id,
                email = innerResponseModel.Identity.EmailAddress,
                name = $"{innerResponseModel.Identity.FirstName} {innerResponseModel.Identity.LastName}"
            };
            return new OkObjectResult(responseModel);
        }
    }
}
The model classes are implemented as follows.
using Newtonsoft.Json;
namespace SignInWithBasecamp
{
    public class BasecampAccessTokenResponseModel
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    }
    public class BasecampAccessTokenErrorResponseModel
    {
        [JsonProperty("error")]
        public string Error { get; set; }
        [JsonProperty("error_description")]
        public long ErrorDescription { get; set; }
    }
    public class BasecampClaimsResponseModel
    {
        [JsonProperty("identity")]
        public BasecampIdentityModel Identity { get; set; }
    }
}
The ForbiddenResult class is implemented as follows.
using Microsoft.AspNetCore.Mvc;
namespace SignInWithBasecamp
{
    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 SignInWithBasecamp
{
    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 Basecamp application
- Log in to https://integrate.37signals.com/.
 - On the Your Applications page, select Register one now.
 - On the New Application page, enter the following fields and then select Register this app:
- Name of your application
 - Your company’s name
 - Your website URL
 - Products: Select Basecamp 3.
 - Redirect URI: 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.
 
 - On the Your Applications page, select the application name.
 - On the application details page, copy the Client ID and Client Secret fields.
 
Add the client secret for the Basecamp 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: 
BasecampClientSecret - Secret: Paste the Client Secret field that was copied in the previous section.
 
 - Options: 
 
Configure Basecamp 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-basecamp-client-idwith the Client ID field that was copied in the Configure a Basecamp application section. Replaceyour-function-app-namewith the function app name that was created in the Create Azure functions section. 
<ClaimsProvider>
  <Domain>37signals.com</Domain>
  <DisplayName>Basecamp</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="Basecamp-OAuth2">
      <DisplayName>Basecamp</DisplayName>
      <Protocol Name="OAuth2" />
      <Metadata>
        <Item Key="authorization_endpoint">https://launchpad.37signals.com/authorization/new?type=web_server</Item>
        <Item Key="client_id">your-basecamp-client-id</Item>
        <Item Key="HttpBinding">POST</Item>
        <Item Key="AccessTokenEndpoint">https://your-function-app-name.azurewebsites.net/api/GetBasecampAccessToken</Item>
        <Item Key="ClaimsEndpoint">https://your-function-app-name.azurewebsites.net/api/GetBasecampClaims</Item>
        <Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item>
        <Item Key="ProviderName">37signals.com</Item>
        <Item Key="UsePolicyInRedirectUri">false</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="client_secret" StorageReferenceId="B2C_1A_BasecampClientSecret" />
      </CryptographicKeys>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialAccountAuthentication" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="37signals.com" 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"BasecampSignUpOrSignIn". 
Add the identity provider to the user journey
- Add the claims provider that was configured in the Configure Basecamp 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="BasecampExchange" />
  </ClaimsProviderSelections>
  ...
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
  ...
  <ClaimsExchanges>
    <ClaimsExchange Id="BasecampExchange" TechnicalProfileReferenceId="Basecamp-OAuth2" />
  </ClaimsExchanges>
  ...
</OrchestrationStep>
Configure the relying party policy
- Open the SignUpOrSignIn.xml file.
 - Replace the ReferenceId attribute for the DefaultUserJourney element from 
"SignUpOrSignIn"to"BasecampSignUpOrSignIn". 
<RelyingParty>
  <DefaultUserJourney ReferenceId="BasecampSignUpSignIn" />
  ...
</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.