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/oauth2https://<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 theopenidscope 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.