This article describes how to add authentication for a Discord account to a sign-up and sign-in flow in an external tenant.
The code for this article can found in https://github.com/chrispadgettlivecom/ciam-functions.
Architecture
The following diagram illustrates the sign-up and sign-in flow.
This sign-up and sign-in flow requires an Azure function that retrieves claims for a Discord user and then returns an ID token containing these user claims to an external tenant.
For information about Discord and OAuth 2.0, see https://discord.com/developers/docs/topics/oauth2.
Prerequisites
Create the Discord functions
Create a functions application
- Create a functions application. This functions application must be created in the Basic tier or higher.
- Create a self-signed certificate and then export the public certificate with the private key. This self-signed certificate is used to sign the ID tokens, which contain claims that are retrieved for a Discord user, that are returned to the external tenant.
- Add the self-signed certificate to the functions application and then configure this self-signed certificate for use in the function code.
- Add the self-signed certificate’s thumbprint as the DISCORD_SIGNING_CERTIFICATE_THUMBPRINT app setting to the functions application.
[
...
{
"name": "DISCORD_SIGNING_CERTIFICATE_THUMBPRINT",
"value": "<your-certificate-thumbprint>",
"slotSetting": false
},
]
Create an OpenID Connect configuration function
Create a C# function containing the following code. This implements a GetDiscordOpenIdConnectConfiguration function that returns the OpenID Connect configuration information for the Discord identity provider to the external tenant. Then publish this C# functions to the functions application.
using CiamFunctionsApplication.Extensions.Http;
using CiamFunctionsApplication.Models.Shared;
using CiamFunctionsApplication.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace CiamFunctionsApplication.Functions.Discord
{
public class GetDiscordOpenIdConnectConfigurationFunction
{
private readonly ILogger<GetDiscordOpenIdConnectConfigurationFunction> _logger;
private readonly ISigningCredentialsProvider _signingCredentialsProvider;
public GetDiscordOpenIdConnectConfigurationFunction(ILogger<GetDiscordOpenIdConnectConfigurationFunction> logger, ISigningCredentialsProvider signingCredentialsProvider)
{
_logger = logger;
_signingCredentialsProvider = signingCredentialsProvider;
}
[Function("GetDiscordOpenIdConnectConfiguration")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "discord/.well-known/openid-configuration")] HttpRequest request)
{
var responseModel = new GetOpenIdConnectConfigurationResponseModel
{
Issuer = $"{request.GetBaseUrl()}api/discord",
AuthorizationEndpoint = "https://discord.com/api/oauth2/authorize",
TokenEndpoint = $"{request.GetBaseUrl()}api/discord/GetDiscordClaims",
TokenEndpointAuthMethodsSupported =
[
"client_secret_post"
],
JwksUri = $"{request.GetBaseUrl()}api/discord/.well-known/keys",
ResponseTypesSupported =
[
"code"
],
SubjectTypesSupported =
[
"public"
]
};
var signingCredentials = _signingCredentialsProvider.GetSigningCredentials("DISCORD_SIGNING_CERTIFICATE_THUMBPRINT");
responseModel.IdTokenSigningAlgValuesSupported =
[
signingCredentials.Algorithm
];
return new OkObjectResult(responseModel);
}
}
}
The GetOpenIdConnectConfigurationResponseModel class is implemented as follows:
using System.Text.Json.Serialization;
namespace CiamFunctionsApplication.Models.Shared
{
public class GetOpenIdConnectConfigurationResponseModel
{
[JsonPropertyName("issuer")]
public string? Issuer { get; set; }
[JsonPropertyName("authorization_endpoint")]
public string? AuthorizationEndpoint { get; set; }
[JsonPropertyName("token_endpoint")]
public string? TokenEndpoint { get; set; }
[JsonPropertyName("token_endpoint_auth_methods_supported")]
public ICollection<string>? TokenEndpointAuthMethodsSupported { get; set; }
[JsonPropertyName("jwks_uri")]
public string? JwksUri { get; set; }
[JsonPropertyName("response_types_supported")]
public ICollection<string>? ResponseTypesSupported { get; set; }
[JsonPropertyName("subject_types_supported")]
public ICollection<string>? SubjectTypesSupported { get; set; }
[JsonPropertyName("id_token_signing_alg_values_supported")]
public ICollection<string>? IdTokenSigningAlgValuesSupported { get; set; }
}
}
Create a JSON Web Key Set function
Create a C# function containing the following code. This implements a GetDiscordJsonWebKeySet function that returns the JSON Web Key Set (JWKS) information for the Discord identity provider to the external tenant. Then publish this C# functions to the functions application.
using System.Security.Cryptography.X509Certificates;
using CiamFunctionsApplication.Models.Shared;
using CiamFunctionsApplication.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using JsonWebKey = CiamFunctionsApplication.Models.JsonWebKey;
namespace CiamFunctionsApplication.Functions.Discord
{
public class GetDiscordJsonWebKeySetFunction
{
private readonly ILogger<GetDiscordJsonWebKeySetFunction> _logger;
private readonly ISigningCredentialsProvider _signingCredentialsProvider;
public GetDiscordJsonWebKeySetFunction(ILogger<GetDiscordJsonWebKeySetFunction> logger, ISigningCredentialsProvider signingCredentialsProvider)
{
_logger = logger;
_signingCredentialsProvider = signingCredentialsProvider;
}
[Function("GetDiscordJsonWebKeySet")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "discord/.well-known/keys")] HttpRequest request)
{
var responseModel = new GetJsonWebKeySetResponseModel();
try
{
var signingCredentials = _signingCredentialsProvider.GetSigningCredentials("DISCORD_SIGNING_CERTIFICATE_THUMBPRINT") as X509SigningCredentials;
var signingCertificate = signingCredentials!.Certificate;
var signingCertificateHash = signingCertificate.GetCertHash();
var signingCertificatePublicKey = signingCertificate.GetRSAPublicKey();
var signingCertificatePublicKeyParameters = signingCertificatePublicKey!.ExportParameters(false);
responseModel.Keys =
[
new JsonWebKey
{
Alg = signingCredentials.Algorithm,
E = Base64UrlEncoder.Encode(signingCertificatePublicKeyParameters.Exponent),
Kid = signingCredentials.Kid,
Kty = "RSA",
N = Base64UrlEncoder.Encode(signingCertificatePublicKeyParameters.Modulus),
Nbf = new DateTimeOffset(signingCertificate.NotBefore).ToUnixTimeSeconds(),
Use = "sig",
X5c =
[
Convert.ToBase64String(signingCertificate.Export(X509ContentType.Cert))
],
X5t = Base64UrlEncoder.Encode(signingCertificateHash)
}
];
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
return new OkObjectResult(responseModel);
}
}
}
The GetJsonWebKeySetResponseModel and JsonWebKey classes are implemented as follows:
using System.Text.Json.Serialization;
namespace CiamFunctionsApplication.Models
{
public class GetJsonWebKeySetResponseModel
{
[JsonPropertyName("keys")]
public ICollection<JsonWebKey>? Keys { get; set; }
}
}
and
using System.Text.Json.Serialization;
namespace CiamFunctionsApplication.Models
{
public class JsonWebKey
{
[JsonPropertyName("alg")]
public string? Alg { get; set; }
[JsonPropertyName("e")]
public string? E { get; set; }
[JsonPropertyName("kid")]
public string? Kid { get; set; }
[JsonPropertyName("kty")]
public string? Kty { get; set; }
[JsonPropertyName("n")]
public string? N { get; set; }
[JsonPropertyName("nbf")]
public long? Nbf { get; set; }
[JsonPropertyName("use")]
public string? Use { get; set; }
[JsonPropertyName("x5c")]
public ICollection<string>? X5c { get; set; }
[JsonPropertyName("x5t")]
public string? X5t { get; set; }
}
}
Create a claims function
Create a C# function containing the following code. This implements a GetDiscordClaims function that retrieves claims for a Discord user and then returns an ID token containing these user claims to the external tenant. Then publish this C# functions to the functions application.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using CiamFunctionsApplication.Models.Discord;
using CiamFunctionsApplication.Models.Shared;
using CiamFunctionsApplication.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace CiamFunctionsApplication.Functions.Discord
{
public class GetDiscordClaimsFunction
{
private readonly ILogger<GetDiscordClaimsFunction> _logger;
private readonly ISigningCredentialsProvider _signingCredentialsProvider;
public GetDiscordClaimsFunction(ILogger<GetDiscordClaimsFunction> logger, ISigningCredentialsProvider signingCredentialsProvider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_signingCredentialsProvider = signingCredentialsProvider ?? throw new ArgumentNullException(nameof(signingCredentialsProvider));
}
[Function("GetDiscordClaims")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "discord/GetDiscordClaims")] HttpRequest request)
{
var requestForm = await request.ReadFormAsync();
using (var httpClient = new HttpClient())
{
var tokenRequestData = new List<KeyValuePair<string, string>>
{
new("grant_type", requestForm["grant_type"].FirstOrDefault()!),
new("client_id", requestForm["client_id"].FirstOrDefault()!),
new("client_secret", requestForm["client_secret"].FirstOrDefault()!),
new("redirect_uri", requestForm["redirect_uri"].FirstOrDefault()!),
new("code", requestForm["code"].FirstOrDefault()!),
new("code_verifier", requestForm["code_verifier"].FirstOrDefault()!)
};
using (var tokenRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://discord.com/api/oauth2/token")
{
Content = new FormUrlEncodedContent(tokenRequestData)
})
{
using (var tokenResponseMessage = await httpClient.SendAsync(tokenRequestMessage))
{
var tokenResponseContent = await tokenResponseMessage.Content.ReadAsStringAsync();
var tokenResponseData = JsonSerializer.Deserialize<TokenResponseModel>(tokenResponseContent);
using (var userInformationRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me"))
{
userInformationRequestMessage.Headers.Add("Authorization", $"Bearer {tokenResponseData!.AccessToken}");
using (var userInformationResponseMessage = await httpClient.SendAsync(userInformationRequestMessage))
{
var userInformationResponseContent = await userInformationResponseMessage.Content.ReadAsStringAsync();
var userInformationResponseData = JsonSerializer.Deserialize<UserInfoResponseModel>(userInformationResponseContent);
var nowUtc = DateTimeOffset.UtcNow;
var idTokenClaims = new List<Claim>
{
new("sub", userInformationResponseData!.Id!),
new("iat", nowUtc.ToUnixTimeSeconds().ToString().ToLowerInvariant(), ClaimValueTypes.Integer64),
new("email", userInformationResponseData.Email!),
new("email_verified", true.ToString().ToLowerInvariant(), ClaimValueTypes.Boolean)
};
var idToken = new JwtSecurityToken(
issuer: $"{request.GetBaseUrl()}api/discord",
audience: requestForm["client_id"].FirstOrDefault(),
claims: idTokenClaims,
notBefore: nowUtc.UtcDateTime,
expires: nowUtc.AddSeconds(300).UtcDateTime,
signingCredentials: _signingCredentialsProvider.GetSigningCredentials("DISCORD_SIGNING_CERTIFICATE_THUMBPRINT") as X509SigningCredentials
);
var idTokenHandler = new JwtSecurityTokenHandler();
var responseModel = new GetClaimsResponseModel
{
TokenType = tokenResponseData.TokenType,
AccessToken = tokenResponseData.AccessToken,
ExpiresIn = tokenResponseData.ExpiresIn,
IdToken = idTokenHandler.WriteToken(idToken)
};
return new OkObjectResult(responseModel);
}
}
}
}
}
}
}
}
The GetClaimsResponseModel class is implemented as follows:
using System.Text.Json.Serialization;
namespace CiamFunctionsApplication.Models
{
public class GetClaimsResponseModel
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string? TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int? ExpiresIn { get; set; }
[JsonPropertyName("id_token")]
public string? IdToken { get; set; }
}
}
Create the signing credentials provider
Create the ISigningCredentialsProvider interface using the following code.
using Microsoft.IdentityModel.Tokens;
namespace CiamFunctionsApplication.Providers
{
public interface ISigningCredentialsProvider
{
SigningCredentials GetSigningCredentials(string certificateThumbprintEnvironmentVariableName);
}
}
Create the X509SigningCredentialsProvider class using the following code.
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
namespace CiamFunctionsApplication.Providers
{
internal class X509SigningCredentialsProvider : ISigningCredentialsProvider
{
public SigningCredentials GetSigningCredentials(string certificateThumbprintEnvironmentVariableName)
{
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, Environment.GetEnvironmentVariable(certificateThumbprintEnvironmentVariableName)!, false);
if (certificates.Count == 0)
{
throw new Exception("Certificate not found.");
}
var certificate = certificates[0];
return new X509SigningCredentials(certificate);
}
}
}
Add the X509SigningCredentialsProvider class as a singleton service to the functions application.
using CiamFunctionsApplication.Providers;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services.AddSingleton<ISigningCredentialsProvider, X509SigningCredentialsProvider>();
})
.Build();
host.Run();
Configure a Discord application
- Log in to https://discordapp.com/developers/applications/.
- On the Applications page, select New Application. In the Create An Application dialogue, enter an application name and then select Create.
- On the General Information page, select OAuth2.
- On the OAuth2 page, copy the Client ID field.
- On the OAuth2 page, select Reset Secret. In the Regenerate Secret Key? dialogue, select Yes, do it! On the OAuth2 page, copy the Client Secret field.
- On the OAuth2 page, select Add Redirect, enter the following redirect URLs and then select Save Changes:
- If you use the built-in domain:
https://<your-tenant-subdomain>.ciamlogin.com/<
your-
tenant-id>/federation/oauth2https://<
your-
tenant-subdomain>.ciamlogin.com/<your-
tenant-subdomain>.onmicrosoft.com/federation/oauth2
- If you use a custom domain:
https://<your-custom-domain-name>/<your-tenant-id>/federation/oauth2
https://<your-custom-domain-name>/<your-tenant-subdomain>.onmicrosoft.com/federation/oauth2
- If you use the built-in domain:
Configure Discord as an identity provider
- Log in to Microsoft Entra Admin Center.
- On the Home page, select Identity, External Identities, and then All identity providers.
- On the All Identity providers page, select Custom. In the Custom tab, select Add new > OpenID Connect.
- In the Add OpenID Connect pane and then the Basics tab, enter the following fields and then select Review + create:
- Display name: Discord
- Well-known endpoint:
https://<your-functions-application-name>.azurewebsites.net/api/discord/.well-known/openid-configuration
- Issuer URI:
https://<your-functions-application-name>.azurewebsites.net/api/discord
- Client ID: The Client ID field that was copied in the Configure a Discord application section.
- Client authentication:
client_secret
- Client secret: The Client Secret field that was copied in the Configure a Discord application section.
- Scope:
openid identify email
. This scope field must contain theopenid
scope in order to receive the ID token from the Azure function. - Response type:
code
- In the Add OpenID Connect pane and then the Review tab, select Create.
Add the Discord identity provider to the sign-up and sign-in user flow
- Log in to Microsoft Entra Admin Center.
- On the Home page, select Identity, External Identities, and then User flows.
- On the User flows page, select the sign-up and sign-in user flow to which the Discord identity provider is to be added.
- On the user flow page, select Identity providers.
- On the Identity providers page, select the Discord identity provider and then select Save.