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/source/Client.AzureAD/Client.AzureAD.csproj b/source/Client.AzureAD/Client.AzureAD.csproj index 0fee24f..9d89ea7 100644 --- a/source/Client.AzureAD/Client.AzureAD.csproj +++ b/source/Client.AzureAD/Client.AzureAD.csproj @@ -24,5 +24,9 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + 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/Client.GoogleApps/Client.GoogleApps.csproj b/source/Client.GoogleApps/Client.GoogleApps.csproj index 1751093..c99c2b4 100644 --- a/source/Client.GoogleApps/Client.GoogleApps.csproj +++ b/source/Client.GoogleApps/Client.GoogleApps.csproj @@ -23,5 +23,9 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + diff --git a/source/Client.OctopusID/Client.OctopusID.csproj b/source/Client.OctopusID/Client.OctopusID.csproj index 62e81e2..51f7e7b 100644 --- a/source/Client.OctopusID/Client.OctopusID.csproj +++ b/source/Client.OctopusID/Client.OctopusID.csproj @@ -23,5 +23,9 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + diff --git a/source/Client.Okta/Client.Okta.csproj b/source/Client.Okta/Client.Okta.csproj index c3956d5..59efa9b 100644 --- a/source/Client.Okta/Client.Okta.csproj +++ b/source/Client.Okta/Client.Okta.csproj @@ -23,5 +23,9 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + diff --git a/source/Client.OpenIDConnect/Client.OpenIDConnect.csproj b/source/Client.OpenIDConnect/Client.OpenIDConnect.csproj index 3a9c2c6..ceb15fa 100644 --- a/source/Client.OpenIDConnect/Client.OpenIDConnect.csproj +++ b/source/Client.OpenIDConnect/Client.OpenIDConnect.csproj @@ -15,13 +15,13 @@ https://github.com/OctopusDeploy/OpenIDConnectAuthenticationProviders - - - - - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + 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..f30a99f 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,26 @@ 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 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/Configuration/MapFromAzureADConfigurationResourceToAzureADConfiguration.cs b/source/Server.AzureAD/Configuration/MapFromAzureADConfigurationResourceToAzureADConfiguration.cs new file mode 100644 index 0000000..a282548 --- /dev/null +++ b/source/Server.AzureAD/Configuration/MapFromAzureADConfigurationResourceToAzureADConfiguration.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Octopus.Core.Infrastructure.Mapping; +using Octopus.Data.Model; + +namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration +{ + class MapFromAzureADConfigurationResourceToAzureADConfiguration : IMapToNew, IMapToExisting + { + public async Task Map(AzureADConfigurationResource source, CancellationToken cancellationToken) + { + await Task.CompletedTask; + var target = new AzureADConfiguration(); + + target.IsEnabled = source.IsEnabled; + target.Issuer = source.Issuer; + target.RoleClaimType = source.RoleClaimType; + target.AllowAutoUserCreation = source.AllowAutoUserCreation ?? false; + target.ClientId = source.ClientId; + + if (source.ClientSecret is { HasValue: true, NewValue: { } }) + { + target.ClientSecret = source.ClientSecret.NewValue.ToSensitiveString(); + } + + if (source.ClientSecret is not { HasValue: true }) + { + target.ClientSecret = null; + } + + if (source.ClientKey is { HasValue: true, NewValue: { } }) + { + target.ClientKey = source.ClientKey.NewValue.ToSensitiveString(); + } + + if (source.ClientKey is not { HasValue: true }) + { + target.ClientKey = null; + } + + return target; + } + + public async Task Map(AzureADConfigurationResource source, AzureADConfiguration target, CancellationToken cancellationToken) + { + await Task.CompletedTask; + + target.IsEnabled = source.IsEnabled; + target.Issuer = source.Issuer; + target.RoleClaimType = source.RoleClaimType; + target.AllowAutoUserCreation = source.AllowAutoUserCreation ?? false; + target.ClientId = source.ClientId; + + if (source.ClientSecret is { HasValue: true, NewValue: { } }) + { + target.ClientSecret = source.ClientSecret.NewValue.ToSensitiveString(); + } + + if (source.ClientSecret is not { HasValue: true }) + { + target.ClientSecret = null; + } + + if (source.ClientKey is { HasValue: true, NewValue: { } }) + { + target.ClientKey = source.ClientKey.NewValue.ToSensitiveString(); + } + + if (source.ClientKey is not { HasValue: true }) + { + target.ClientKey = null; + } + } + } +} diff --git a/source/Server.AzureAD/Configuration/MapFromAzureADConfigurationToAzureADConfigurationResource.cs b/source/Server.AzureAD/Configuration/MapFromAzureADConfigurationToAzureADConfigurationResource.cs new file mode 100644 index 0000000..d9c3354 --- /dev/null +++ b/source/Server.AzureAD/Configuration/MapFromAzureADConfigurationToAzureADConfigurationResource.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Octopus.Core.Infrastructure.Mapping; +using Octopus.Server.MessageContracts; + +namespace Octopus.Server.Extensibility.Authentication.AzureAD.Configuration +{ + class MapFromAzureADConfigurationToAzureADConfigurationResource : IMapToNew + { + public async Task Map(AzureADConfiguration source, CancellationToken cancellationToken) + { + await Task.CompletedTask; + + var target = new AzureADConfigurationResource(); + + target.Id = source.Id; + target.IsEnabled = source.IsEnabled; + target.AllowAutoUserCreation = source.AllowAutoUserCreation; + target.RoleClaimType = source.RoleClaimType; + target.Issuer = source.Issuer; + target.ClientId = source.ClientId; + + if (source.ClientSecret == null) + { + target.ClientSecret = null; + } + else + { + target.ClientSecret = !string.IsNullOrWhiteSpace(source.ClientSecret?.Value) ? new SensitiveValue { HasValue = true } : new SensitiveValue { HasValue = false }; + } + + if (source.ClientKey == null) + { + target.ClientKey = null; + } + else + { + target.ClientKey = !string.IsNullOrWhiteSpace(source.ClientKey?.Value) ? new SensitiveValue { HasValue = true } : new SensitiveValue { HasValue = false }; + } + + return target; + } + } +} diff --git a/source/Server.AzureAD/Server.AzureAD.csproj b/source/Server.AzureAD/Server.AzureAD.csproj index e3821e7..3dbecde 100644 --- a/source/Server.AzureAD/Server.AzureAD.csproj +++ b/source/Server.AzureAD/Server.AzureAD.csproj @@ -14,9 +14,9 @@ true - - - + + 1701;1702 + @@ -24,8 +24,42 @@ - - - - + + + C:\Program Files\Octopus Deploy\Octopus\Newtonsoft.Json.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Core.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.OpenIDConnect.Common.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + 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 diff --git a/source/Server.AzureAD/Web/AzureADUserAuthenticatedAction.cs b/source/Server.AzureAD/Web/AzureADUserAuthenticatedAction.cs index 6f9248d..8c2bbf6 100644 --- a/source/Server.AzureAD/Web/AzureADUserAuthenticatedAction.cs +++ b/source/Server.AzureAD/Web/AzureADUserAuthenticatedAction.cs @@ -1,4 +1,5 @@ -using Octopus.Diagnostics; +using Octopus.Core.Authentication; +using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.AzureAD.Configuration; using Octopus.Server.Extensibility.Authentication.AzureAD.Identities; using Octopus.Server.Extensibility.Authentication.AzureAD.Infrastructure; @@ -22,7 +23,8 @@ public AzureADUserAuthenticatedAction( ISleep sleep, IAzureADIdentityCreator identityCreator, IUrlEncoder encoder, - IUserService userService) : + IUserService userService, + IOctopusAuthenticationConfigurationStore authenticationConfigurationStore) : base( log, authTokenHandler, @@ -33,7 +35,8 @@ public AzureADUserAuthenticatedAction( sleep, identityCreator, encoder, - userService) + userService, + authenticationConfigurationStore) { } diff --git a/source/Server.AzureAD/Web/AzureADUserAuthenticatedPkceAction.cs b/source/Server.AzureAD/Web/AzureADUserAuthenticatedPkceAction.cs index a8c5ab7..7360780 100644 --- a/source/Server.AzureAD/Web/AzureADUserAuthenticatedPkceAction.cs +++ b/source/Server.AzureAD/Web/AzureADUserAuthenticatedPkceAction.cs @@ -1,4 +1,5 @@ -using Octopus.Diagnostics; +using Octopus.Core.Authentication; +using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.AzureAD.Configuration; using Octopus.Server.Extensibility.Authentication.AzureAD.Identities; using Octopus.Server.Extensibility.Authentication.AzureAD.Infrastructure; @@ -26,7 +27,8 @@ public AzureADUserAuthenticatedPkceAction( IUrlEncoder encoder, IIdentityProviderConfigDiscoverer identityProviderConfigDiscoverer, IMediator mediator, - IUserService service) + IUserService service, + IOctopusAuthenticationConfigurationStore authenticationConfigurationStore) : base(log, authTokenHandler, principalToUserResourceMapper, @@ -38,7 +40,8 @@ public AzureADUserAuthenticatedPkceAction( encoder, identityProviderConfigDiscoverer, mediator, - service) + service, + authenticationConfigurationStore) { } diff --git a/source/Server.GoogleApps/Server.GoogleApps.csproj b/source/Server.GoogleApps/Server.GoogleApps.csproj index dd1d384..9330c73 100644 --- a/source/Server.GoogleApps/Server.GoogleApps.csproj +++ b/source/Server.GoogleApps/Server.GoogleApps.csproj @@ -36,5 +36,33 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + diff --git a/source/Server.OctopusID/Server.OctopusID.csproj b/source/Server.OctopusID/Server.OctopusID.csproj index d32bc47..d4071f4 100644 --- a/source/Server.OctopusID/Server.OctopusID.csproj +++ b/source/Server.OctopusID/Server.OctopusID.csproj @@ -14,10 +14,6 @@ true - - - - @@ -27,5 +23,33 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + \ No newline at end of file diff --git a/source/Server.Okta/Server.Okta.csproj b/source/Server.Okta/Server.Okta.csproj index faa294c..54495b6 100644 --- a/source/Server.Okta/Server.Okta.csproj +++ b/source/Server.Okta/Server.Okta.csproj @@ -14,10 +14,6 @@ true - - - - @@ -28,5 +24,33 @@ - + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + \ No newline at end of file diff --git a/source/Server.OpenIDConnect.Common/Server.OpenIDConnect.Common.csproj b/source/Server.OpenIDConnect.Common/Server.OpenIDConnect.Common.csproj index 8d43ece..895f129 100644 --- a/source/Server.OpenIDConnect.Common/Server.OpenIDConnect.Common.csproj +++ b/source/Server.OpenIDConnect.Common/Server.OpenIDConnect.Common.csproj @@ -14,12 +14,7 @@ - - - - - @@ -30,4 +25,33 @@ + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + \ No newline at end of file diff --git a/source/Server.OpenIDConnect.Common/Web/AuthServerResponseHandler.cs b/source/Server.OpenIDConnect.Common/Web/AuthServerResponseHandler.cs index 6bb75a0..9a66098 100644 --- a/source/Server.OpenIDConnect.Common/Web/AuthServerResponseHandler.cs +++ b/source/Server.OpenIDConnect.Common/Web/AuthServerResponseHandler.cs @@ -88,7 +88,7 @@ public InvalidLoginAction CheckIfAuthenticationAttemptIsBanned(string username, public IOctoResponseProvider Success(IOctoRequest request, ISuccessResult successResult, string username, LoginState state) { - loginTracker.RecordSucess(username, request.Host); + loginTracker.RecordSuccess(username, request.Host); UserAuthenticatedValidator.ValidateUserIsActive(successResult.Value.IsActive, username); UserAuthenticatedValidator.ValidateUserIsNotServiceAccount(successResult.Value.IsService, username); diff --git a/source/Tests/Tests.csproj b/source/Tests/Tests.csproj index 236165f..ac363e3 100644 --- a/source/Tests/Tests.csproj +++ b/source/Tests/Tests.csproj @@ -23,4 +23,33 @@ + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Data.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Extensibility.Authentication.dll + + + C:\Program Files\Octopus Deploy\Octopus\Autofac.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.Client.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Server.MessageContracts.Base.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Diagnostics.dll + + + C:\Program Files\Octopus Deploy\Octopus\Octopus.Time.dll + + \ No newline at end of file