diff --git a/Update-OctopusReferences.ps1 b/Update-OctopusReferences.ps1 new file mode 100644 index 0000000..f12a077 --- /dev/null +++ b/Update-OctopusReferences.ps1 @@ -0,0 +1,8 @@ +# Run this script after installing a new version of Octopus Server, but before starting it (due to file lock and dependency load issues). + +$OctopusServerBinaryLocation = "E:\Program Files\Octopus Deploy\Octopus" # Make sure this points to where you install the Octopus Server binaries +$OctopusServerCustomExtensionsLocation = "C:\ProgramData\Octopus\CustomExtensions\" # This should always point to where the Octopus Server CustomExtenions folder lives + +Copy-Item "$OctopusServerBinaryLocation\Octopus.Data.dll" -Destination "$OctopusServerCustomExtensionsLocation" -Force +Copy-Item "$OctopusServerBinaryLocation\Octopus.Server.Extensibility.Authentication.dll" -Destination "$OctopusServerCustomExtensionsLocation" -Force +Copy-Item "$OctopusServerBinaryLocation\BuiltInExtensions\Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.dll" -Destination "$OctopusServerCustomExtensionsLocation" -Force diff --git a/readme.md b/readme.md index c5e3f44..ee4352e 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,46 @@ +# Octopus Deploy - Microsoft AzureAD GraphAPI support for Group Membership Overages in Auth Token + +## AzureAD Group Role Token Limitations +There are two primary ways to interface with AzureAD, SAML or OpenID (Octopus uses OpenID). Both of these formats present a token that's sent back with various information that describes an authenticated user. This information is called claims. If implementing group based role claims in Octopus, AzureAD will add to the JWT OpenID token a listing of the AzureAD group IDs the user is a member of. This is great, as Octopus uses this information to cross check which Octopus Teams a user belongs to. But there are limitations with both SAML and OpenID tokens. SAML limits the amount of groups returned to 150. OpenID JWT tokens are lmited to 200 groups. In large organizations and Enterprises users often exceed these limitations by SAML and OpenID. + +There are ways to wildcard limit how many groups are returned in AzureAD tokens based on a prefix. For example "devops_[groupNameHere]". But this feature in AzureAD does not work for OpenID. And Octopus only implements OpenID with its AzureAD implementation. Plus it's still (as of the writing of this) a preview feature in AzureAD for SAML. + +## Microsoft/AzureAD's "Solution" to this Limitation +If there are too many groups returned in a token sent by AzureAD. Microsoft includes a custom claim "_claims_name" & "_claims_sources" that has a link to the Azure GraphAPI (an old deprecated one - more on that below) that has the group membership list for the user. They (Microsoft) expect applications to implement this in their code when developing AzureAD integrations. + +See more [here](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/5-WebApp-AuthZ/5-2-Groups#processing-groups-claim-in-tokens-including-handling-overage). They also include an example ASP.NET Core application code that implements this [here](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphHelper.cs). + +## The Reason for this Octopus OpenID Auth Extension Fork +The primary reason for this fork is to implement this "solution" from Microsoft. The code in this fork is (except where otherwise stated) exactly the same as the [official repo from Octopus Deploy](https://github.com/OctopusDeploy/OpenIDConnectAuthenticationProviders). But it now has the ability to follow the Microsoft GraphAPI if it detects group membership overages. I will try my best to keep this fork in lock step with Octopus Deploy's. **But the overall goal of this fork is to eventually have Octopus Deploy merge it back into the primary OpenID Auth Provider repo provided by Octopus Deploy. That or at the very least provide a template on how to implement this.** + +## Microsoft GraphAPI vs. Windows GraphAPI +I mentioned above that I am not following the exact Azure GraphAPI endpoint that is provided in the token. This is because (for whatever reason) Microsoft hasn't updated the backend to provide the new GraphAPI endpoint. Microsoft is slowly deprecating https://graph.windows.net in favor of https://graph.microsoft.com. This decision (to use graph.microsoft.com) is to future proof this implementation. This also affects the permissions the App Registration will need that you use for your AzureAD Octopus logins (more on that below). + +## Installation, Configuration and Usage +### Installation: +I had all kinds of problems getting this customized AzureAD extension to work. Sadly it wasn't as easy as stated on the [Octopus Documentation page](https://octopus.com/docs/administration/server-extensibility/installing-a-custom-server-extension). I had to write a custom PowerShell script that copied the .dll's it needed to function from the Octopus install path into the Octopus CustomExtensions path. + +1. At the root of this repo there is a PowerShell script named "Update-OctopusReferences.ps1". Place this in the CustomExtensions folder located here "_%ProgramData%\Octopus\CustomExtensions_" on your Octopus Server. Also make sure the file paths listed in this PowerShell script at the top are correct. This PowerShell script will basically copy a few .dll dependencies from the root Octopus Server install folder to this "_CustomExtensions_" folder to allow the modified AzureAD extension to run without erroring out. +2. Build the code in this fork using the documentation listed [here](https://octopus.com/docs/administration/server-extensibility/customizing-an-octopus-deploy-server-extension). +3. Copy the newly built dll file named "_Octopus.Server.Extensibility.Authentication.AzureAD.dll_" to the "_%ProgramData%\Octopus\CustomExtensions_" location on the Octopus Server. Along side the above PowerShell script. +4. Stop the Octopus Server service. +5. Run the PowerShell script with admin privileges. Make sure the Octopus Server Windows Service is not running, or you will run into file lock issues. +6. Start the Octopus Server service back up. +7. Keep in mind that whenever you upgrade your Octopus Server you will need to run this PowerShell script to verify you are running with the latest .dll dependencies from the root Octopus Server install folder. My recommendation is to run it right after the installer completes the file copy but before you click "_Finish_" on the installer. + +### Configuration: +This modified plugin will work exactly like it has before by default. In order to enable the new functionality, you will need to browse to "_Octopus Web UI > Configuration > Settings > AzureAD_" and specify a "_Client Access Key_". This is a secret key that is generated on the Azure App Registration that you use for AzureAD integration. + +To generate a Client Access Key go to the Azure Portal, open up the Azure App Registration for Octopus and click the menu item called "_Certificates & Secrets_". Click the "_New Client Secret_" link towards the bottom to generate a new key. This key needs to be entered into the "_Client Access Key_" field in Octopus AzureAD Settings. + +**Note:** Keep in mind that you may also have to add additional API Permissions to your Octopus App Registration to allow it to access the Microsoft Graph API. In my testing I made sure the following API Permission was present: "Directory.Read.All" (Delegated). + +### Usage: +Once you add this secret key to the Octopus AzureAD configuration section this plugin will now enable the ability to follow the Azure Microsoft GraphAPI if it detects a token that has a group membership overage. If the token does not have an overage, it will not hit the GraphAPI endpoint and will continue to function like it always has. + +======================================================== + +# Original Octopus Deploy OpenID Provider Readme Notes This repository contains the [Octopus Deploy][1] OpenID Connect authentication providers. ## Documentation diff --git a/source/Client.AzureAD/Configuration/AzureADConfigurationResource.cs b/source/Client.AzureAD/Configuration/AzureADConfigurationResource.cs index 6e37802..68cf93b 100644 --- a/source/Client.AzureAD/Configuration/AzureADConfigurationResource.cs +++ b/source/Client.AzureAD/Configuration/AzureADConfigurationResource.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using Octopus.Client.Extensibility.Attributes; using Octopus.Client.Extensibility.Authentication.OpenIDConnect.Configuration; +using Octopus.Client.Model; namespace Octopus.Client.Extensibility.Authentication.AzureAD.Configuration { @@ -16,5 +17,10 @@ public AzureADConfigurationResource() [Description("Tell Octopus how to find the roles/groups in the security token from Azure Active Directory (usually \"roles\" or \"groups\")")] [Writeable] public string RoleClaimType { get; set; } + + [DisplayName("Client Access Key")] + [Description("The Azure app registration secret access key. This is used for authenticating against the Azure GraphAPI for group overage lookups. If left blank it will disable Azure GraphAPI lookups. [Learn more](https://github.com/StephenShamakian/OpenIDConnectAuthenticationProviders#readme)")] + [Writeable] + public SensitiveValue ClientKey { get; set; } } } \ No newline at end of file diff --git a/source/Server.AzureAD/AzureADExtension.cs b/source/Server.AzureAD/AzureADExtension.cs index 71bf19d..925779e 100644 --- a/source/Server.AzureAD/AzureADExtension.cs +++ b/source/Server.AzureAD/AzureADExtension.cs @@ -20,7 +20,7 @@ namespace Octopus.Server.Extensibility.Authentication.AzureAD { - [OctopusPlugin("AzureAD", "Octopus Deploy")] + [OctopusPlugin("AzureAD - GraphAPI Support", "Octopus Deploy (Modified by: Stephen Shamakian)")] public class AzureADExtension : OpenIDConnectExtension, IOctopusExtension { public override void Load(ContainerBuilder builder) diff --git a/source/Server.AzureAD/Configuration/AzureADConfiguration.cs b/source/Server.AzureAD/Configuration/AzureADConfiguration.cs index adfa681..e91c964 100644 --- a/source/Server.AzureAD/Configuration/AzureADConfiguration.cs +++ b/source/Server.AzureAD/Configuration/AzureADConfiguration.cs @@ -1,4 +1,5 @@ -using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; +using Octopus.Data.Model; +using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration { @@ -10,5 +11,7 @@ public AzureADConfiguration() : base(AzureADConfigurationStore.SingletonId, "Azu { RoleClaimType = DefaultRoleClaimType; } + + public SensitiveString? ClientKey { get; set; } } } \ No newline at end of file diff --git a/source/Server.AzureAD/Configuration/AzureADConfigurationResource.cs b/source/Server.AzureAD/Configuration/AzureADConfigurationResource.cs index f2f32ef..924be00 100644 --- a/source/Server.AzureAD/Configuration/AzureADConfigurationResource.cs +++ b/source/Server.AzureAD/Configuration/AzureADConfigurationResource.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; +using Octopus.Server.MessageContracts; using Octopus.Server.MessageContracts.Attributes; namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration @@ -11,5 +12,10 @@ class AzureADConfigurationResource : OpenIDConnectConfigurationResource [Description("Tell Octopus how to find the roles/groups in the security token from Azure Active Directory (usually \"roles\" or \"groups\")")] [Writeable] public string? RoleClaimType { get; set; } + + [DisplayName("Client Access Key")] + [Description("The Azure app registration secret access key. This is used for authenticating against the Azure GraphAPI for group overage lookups. If left blank it will disable Azure GraphAPI lookups. [Learn more](https://github.com/StephenShamakian/OpenIDConnectAuthenticationProviders#readme)")] + [Writeable] + public SensitiveValue? ClientKey { get; set; } } } \ No newline at end of file diff --git a/source/Server.AzureAD/Configuration/AzureADConfigurationSettings.cs b/source/Server.AzureAD/Configuration/AzureADConfigurationSettings.cs index 9f05b52..61ce749 100644 --- a/source/Server.AzureAD/Configuration/AzureADConfigurationSettings.cs +++ b/source/Server.AzureAD/Configuration/AzureADConfigurationSettings.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Octopus.Data.Model; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; using Octopus.Server.Extensibility.Extensions.Infrastructure.Configuration; @@ -21,6 +22,7 @@ public override IEnumerable GetConfigurationValues() yield return configurationValue; } yield return new ConfigurationValue($"Octopus.{ConfigurationDocumentStore.ConfigurationSettingsName}.RoleClaimType", ConfigurationDocumentStore.GetRoleClaimType(), ConfigurationDocumentStore.GetIsEnabled() && ConfigurationDocumentStore.GetRoleClaimType() != AzureADConfiguration.DefaultRoleClaimType, "Role Claim Type"); + yield return new ConfigurationValue($"Octopus.{ConfigurationDocumentStore.ConfigurationSettingsName}.ClientKey", ConfigurationDocumentStore.GetClientKey(), ConfigurationDocumentStore.GetIsEnabled(), "Client Access Key"); } } diff --git a/source/Server.AzureAD/Configuration/AzureADConfigurationStore.cs b/source/Server.AzureAD/Configuration/AzureADConfigurationStore.cs index 5c666d6..b1ed21e 100644 --- a/source/Server.AzureAD/Configuration/AzureADConfigurationStore.cs +++ b/source/Server.AzureAD/Configuration/AzureADConfigurationStore.cs @@ -1,4 +1,6 @@ -using Octopus.Data.Storage.Configuration; +using Octopus.Data.Model; +using Octopus.Data.Storage.Configuration; +using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration @@ -6,14 +8,31 @@ namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration class AzureADConfigurationStore : OpenIDConnectConfigurationWithRoleStore, IAzureADConfigurationStore { public const string SingletonId = "authentication-aad"; + ISystemLog log; public override string Id => SingletonId; public override string ConfigurationSettingsName => "AzureAD"; public AzureADConfigurationStore( - IConfigurationStore configurationStore) : base(configurationStore) + IConfigurationStore configurationStore, ISystemLog log) : base(configurationStore) { + this.log = log; } + + public void SetClientKey(string? clientkey) + { + SetProperty(doc => doc.RoleClaimType = clientkey); + } + + public SensitiveString? GetClientKey() => GetProperty(doc => doc.ClientKey); + + public void SetClientKey(SensitiveString? key) => SetProperty(doc => + { + if (!string.IsNullOrEmpty(key?.Value)) + log.WithSensitiveValue(key.Value); + + doc.ClientKey = key; + }); } } \ No newline at end of file diff --git a/source/Server.AzureAD/Configuration/AzureADConfigureCommands.cs b/source/Server.AzureAD/Configuration/AzureADConfigureCommands.cs index a494459..f601891 100644 --- a/source/Server.AzureAD/Configuration/AzureADConfigureCommands.cs +++ b/source/Server.AzureAD/Configuration/AzureADConfigureCommands.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Octopus.Data.Model; using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; using Octopus.Server.Extensibility.Extensions.Infrastructure.Configuration; @@ -9,12 +10,15 @@ namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration { class AzureADConfigureCommands : OpenIDConnectConfigureCommands { + readonly ISystemLog log; + public AzureADConfigureCommands( ISystemLog log, Lazy configurationStore, Lazy webPortalConfigurationStore) : base(log, configurationStore, webPortalConfigurationStore) { + this.log = log; } protected override string ConfigurationSettingsName => "azureAD"; @@ -30,6 +34,19 @@ public override IEnumerable GetOptions() ConfigurationStore.Value.SetRoleClaimType(v); Log.Info($"{ConfigurationSettingsName} RoleClaimType set to: {v}"); }); + yield return new ConfigureCommandOption($"{ConfigurationSettingsName}ClientKey=", "The App Registration secret access key. Used for authenticating against the GraphAPI for group overage lookups.", v => + { + if (!string.IsNullOrEmpty(v)) + { + ConfigurationStore.Value.SetClientKey(v.ToSensitiveString()); + log.Info("Azure AD Graph API Client Key set to provided value"); + } + else + { + ConfigurationStore.Value.SetClientKey(null); + log.Info("Azure AD Graph API Client Key set to null (anonymous bind)"); + } + }); } } } \ No newline at end of file diff --git a/source/Server.AzureAD/Configuration/IAzureADConfigurationStore.cs b/source/Server.AzureAD/Configuration/IAzureADConfigurationStore.cs index 9f1b8c5..bb0fedf 100644 --- a/source/Server.AzureAD/Configuration/IAzureADConfigurationStore.cs +++ b/source/Server.AzureAD/Configuration/IAzureADConfigurationStore.cs @@ -1,8 +1,13 @@ -using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; +using Octopus.Data.Model; +using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Configuration; namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration { interface IAzureADConfigurationStore : IOpenIDConnectConfigurationWithRoleStore { + + SensitiveString? GetClientKey(); + void SetClientKey(SensitiveString? key); + } } \ No newline at end of file diff --git a/source/Server.AzureAD/Server.AzureAD.csproj b/source/Server.AzureAD/Server.AzureAD.csproj index 13ee9b2..ded2ddc 100644 --- a/source/Server.AzureAD/Server.AzureAD.csproj +++ b/source/Server.AzureAD/Server.AzureAD.csproj @@ -14,6 +14,10 @@ true + + 1701;1702 + + diff --git a/source/Server.AzureAD/Tokens/AzureADAuthTokenHandler.cs b/source/Server.AzureAD/Tokens/AzureADAuthTokenHandler.cs index 0a48b22..c6c9bc6 100644 --- a/source/Server.AzureAD/Tokens/AzureADAuthTokenHandler.cs +++ b/source/Server.AzureAD/Tokens/AzureADAuthTokenHandler.cs @@ -1,15 +1,238 @@ -using Octopus.Diagnostics; +using Newtonsoft.Json; +using Octopus.Data.Model; +using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.AzureAD.Configuration; using Octopus.Server.Extensibility.Authentication.AzureAD.Issuer; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Issuer; using Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; namespace Octopus.Server.Extensibility.Authentication.AzureAD.Tokens { class AzureADAuthTokenHandler : OpenIDConnectAuthTokenWithRolesHandler, IAzureADAuthTokenHandler { public AzureADAuthTokenHandler(ISystemLog log, IAzureADConfigurationStore configurationStore, IIdentityProviderConfigDiscoverer identityProviderConfigDiscoverer, IAzureADKeyRetriever keyRetriever) : base(log, configurationStore, identityProviderConfigDiscoverer, keyRetriever) + {} + + protected class MicrosoftGraphResponse + { + public string? odata {get; set;} + public List? value { get; set; } + } + + protected class MicrosoftGraphTokenResponse + { + public string? token_type { get; set; } + public int expires_in { get; set; } + public int ext_expires_in { get; set; } + public string? access_token { get; set; } + } + + protected async Task FollowGroupApiCall(ClaimsPrincipal principal) { + List groupObjectIds = new List(); + + string? clientId = ConfigurationStore.GetClientId(); + + if (string.IsNullOrWhiteSpace(clientId)) + { + // Failed to get Access Token + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get App Registration Client ID from Octopus Configuration Store!"); + return new string[0]; + } + + String? clientKey = ConfigurationStore.GetClientKey()?.Value; + + if (String.IsNullOrWhiteSpace(clientKey)) + { + // Failed to get Access Token + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get App Registration Client Key from Octopus Configuration Store!"); + return new string[0]; + } + + string? tenantId = null; + if (principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid") != null) + { + tenantId = principal.Claims.FirstOrDefault(c => string.Equals(c.Type, "http://schemas.microsoft.com/identity/claims/tenantid", StringComparison.OrdinalIgnoreCase)).Value; + } + else + { + // Failed to get tenantId from claim data + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get AzureAD TenantID (tid) from claim data. Make sure tenantid is returned in the claim!"); + return new string[0]; + } + + HttpClient client = new HttpClient(); + + // Get Access Token for GraphAPI + HttpRequestMessage requestToken = new HttpRequestMessage(HttpMethod.Post, "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token"); + + var body = new List>(); + body.Add(new KeyValuePair("client_id", clientId)); + body.Add(new KeyValuePair("scope", "https://graph.microsoft.com/.default")); + body.Add(new KeyValuePair("client_secret", clientKey)); + body.Add(new KeyValuePair("grant_type", "client_credentials")); + + requestToken.Content = new FormUrlEncodedContent(body); + HttpResponseMessage responseToken = await client.SendAsync(requestToken); + + // Endpoint returns JSON with an array of Group ObjectIDs + if (responseToken.IsSuccessStatusCode) + { + + string responseTokenContent = await responseToken.Content.ReadAsStringAsync(); + MicrosoftGraphTokenResponse tokenResult = JsonConvert.DeserializeObject(responseTokenContent); + + string accessToken; + + if ((tokenResult.access_token) != null) + { + accessToken = tokenResult.access_token; + } + else + { + // Failed to get Access Token + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get user's Access Token!"); + return new string[0]; + } + + string? userObjectId = null; + if (principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier") != null) + { + userObjectId = principal.Claims.FirstOrDefault(c => string.Equals(c.Type, "http://schemas.microsoft.com/identity/claims/objectidentifier", StringComparison.OrdinalIgnoreCase)).Value; + } + else + { + // Failed to get userObjectId from claim data + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get AzureAD User Object ID (oid) from claim data. Make sure the user oid is returned in the claim!"); + return new string[0]; + } + + string requestUrl = "https://graph.microsoft.com/v1.0/" + tenantId + "/users/" + userObjectId + "/getMemberObjects"; + + // Get Group Membership list from GraphAPI + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + StringContent content = new StringContent("{\"securityEnabledOnly\": \"false\"}"); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); + + // Endpoint returns JSON with an array of Group ObjectIDs + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + MicrosoftGraphResponse groupsResult = JsonConvert.DeserializeObject(responseContent); + + if ((groupsResult.value) != null) + { + foreach (string groupObjectID in groupsResult.value) + { + groupObjectIds.Add(groupObjectID); + } + } + else + { + // Failed to get Group Memberships + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get list of groups from the AzureAD Graph API!"); + return new string[0]; + } + + string[] groups = groupObjectIds.ToArray(); + + return groups; + } + else + { + string responseContent = await response.Content.ReadAsStringAsync(); + + // Failed to get Group Memberships + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get group membership via the AzureAD Graph API!"); + Log.Error("+++ AzureAD-GraphAPI: ERROR - " + responseContent); + return new string[0]; + } + } + else + { + string responseTokenContent = await responseToken.Content.ReadAsStringAsync(); + + // Failed to get Auth Token + Log.Error("+++ AzureAD-GraphAPI: ERROR - Failed to get Auth Token for group membership API!"); + Log.Error("+++ AzureAD-GraphAPI: ERROR - " + responseTokenContent); + return new string[0]; + } + } + + + protected override string[] GetProviderGroupIds(ClaimsPrincipal principal) + { + var roleClaimType = ConfigurationStore.GetRoleClaimType(); + + if (string.IsNullOrWhiteSpace(roleClaimType)) + { + return new string[0]; + } + + // Get Octopus AzureAD COnfiguration - Client Access Key + String? clientKey = ConfigurationStore.GetClientKey()?.Value; + + + // Retrieve "_claims_names" token value if set, if not null + string? claimNames = null; + if (principal.FindFirst("_claim_names") != null) + { + claimNames = principal.Claims.FirstOrDefault(c => string.Equals(c.Type, "_claim_names", StringComparison.OrdinalIgnoreCase)).Value; + } + + + // Get some additional claim data for better logging + string claimUsersEmail = "(Email token not present in claim!)"; + if (principal.FindFirst(ClaimTypes.Email) != null) + { + claimUsersEmail = principal.Claims.FirstOrDefault(c => string.Equals(c.Type, ClaimTypes.Email, StringComparison.OrdinalIgnoreCase)).Value; + } + + string claimUsersOid = "(Object ID (oid) token not present in claim!)"; + if (principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier") != null) + { + claimUsersOid = principal.Claims.FirstOrDefault(c => string.Equals(c.Type, "http://schemas.microsoft.com/identity/claims/objectidentifier", StringComparison.OrdinalIgnoreCase)).Value; + } + + // Only follow the Microsoft GraphAPI for group membership If: + // - Octopus AzureAD Config Client Access Key is set + // - claimNames is not null + // - claimNames has the value of: {"groups":"src1"} + // Else look for group membership in JWT token + if ((!String.IsNullOrWhiteSpace(clientKey)) && (!String.IsNullOrWhiteSpace(claimNames)) && (claimNames == "{\"groups\":\"src1\"}")) + { + + // If this claim has the "_claim_names" present this means this user is over the 150/200 group limit in the token. We need to follow the Microsoft Azure Graph API. But only if the Client Key is set in the AzureAD Octopus configuration. + Log.Info("+++ AzureAD-GraphAPI: UserAuth - Using Azure GraphAPI group lookup endpoint - ("+ claimUsersEmail + " - "+ claimUsersOid + ")"); + + return FollowGroupApiCall(principal).Result; + + } + else + { + + // the groups Ids consist of external Role and Group identifiers. We always load ClaimTypes.Role claims + // as external identifiers, and then also based on a custom claim specified by the provider. + Log.Info("+++ AzureAD-GraphAPI: UserAuth - Using JWT token groups - (" + claimUsersEmail + " - " + claimUsersOid + ")"); + + var groups = principal.FindAll(ClaimTypes.Role) + .Concat(principal.FindAll(roleClaimType)) + .Select(c => c.Value).ToArray(); + + return groups; + + } + } } } \ No newline at end of file