diff --git a/TokenGenerator/GetPersonalToken.cs b/TokenGenerator/GetPersonalToken.cs index e6911f8..fb06bad 100644 --- a/TokenGenerator/GetPersonalToken.cs +++ b/TokenGenerator/GetPersonalToken.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; @@ -9,72 +7,101 @@ using System.Threading.Tasks; using Microsoft.Extensions.Options; using TokenGenerator.Services.Interfaces; +using System.Threading; + +namespace TokenGenerator; -namespace TokenGenerator +public class GetPersonalToken { - public class GetPersonalToken + private readonly IToken tokenHelper; + private readonly IRequestValidator requestValidator; + private readonly IAuthorization authorization; + private readonly IRandomIdentifier randomIdentifier; + private readonly IRegisterService registerService; + private readonly Settings settings; + + public GetPersonalToken(IToken tokenHelper, IRequestValidator requestValidator, IAuthorization authorization, IRandomIdentifier randomIdentifier, IRegisterService registerService, IOptions settings) { - private readonly IToken tokenHelper; - private readonly IRequestValidator requestValidator; - private readonly IAuthorization authorization; - private readonly IRandomIdentifier randomIdentifier; - private readonly Settings settings; + this.tokenHelper = tokenHelper; + this.requestValidator = requestValidator; + this.authorization = authorization; + this.randomIdentifier = randomIdentifier; + this.registerService = registerService; + this.settings = settings.Value; + } - public GetPersonalToken(IToken tokenHelper, IRequestValidator requestValidator, IAuthorization authorization, IRandomIdentifier randomIdentifier, IOptions settings) + [FunctionName(nameof(GetPersonalToken))] + public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req) + { + using var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(req.HttpContext.RequestAborted); + ActionResult failedAuthorizationResult = await authorization.Authorize(settings.AuthorizedScopePersonal); + if (failedAuthorizationResult != null) { - this.tokenHelper = tokenHelper; - this.requestValidator = requestValidator; - this.authorization = authorization; - this.randomIdentifier = randomIdentifier; - this.settings = settings.Value; + return failedAuthorizationResult; } - [FunctionName(nameof(GetPersonalToken))] - public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req) + requestValidator.ValidateQueryParam("env", true, tokenHelper.IsValidEnvironment, out string env); + requestValidator.ValidateQueryParam("scopes", false, tokenHelper.TryParseScopes, out string[] scopes, new[] { "altinn:enduser" }); + requestValidator.ValidateQueryParam("userId", false, uint.TryParse, out uint userId); + requestValidator.ValidateQueryParam("partyId", false, uint.TryParse, out uint partyId); + requestValidator.ValidateQueryParam("pid", false, tokenHelper.IsValidPid, out string pid); + requestValidator.ValidateQueryParam("bulkCount", false, uint.TryParse, out uint bulkCount); + requestValidator.ValidateQueryParam("authLvl", false, tokenHelper.IsValidAuthLvl, out string authLvl, "3"); + requestValidator.ValidateQueryParam("consumerOrgNo", false, tokenHelper.IsValidPidOrOrgNo, out string consumerOrgNo, "991825827"); + requestValidator.ValidateQueryParam("partyuuid", false, Guid.TryParse, out Guid partyUuid); + requestValidator.ValidateQueryParam("userName", false, tokenHelper.IsValidIdentifier, out string userName, ""); + requestValidator.ValidateQueryParam("clientAmr", false, tokenHelper.IsValidIdentifier, out string clientAmr, "virksomhetssertifikat"); + requestValidator.ValidateQueryParam("ttl", false, uint.TryParse, out uint ttl, 1800); + requestValidator.ValidateQueryParam("delegationSource", false, tokenHelper.IsValidUri, out string delegationSource); + requestValidator.ValidateQueryParam("getEnvIds", false, bool.TryParse, out bool getEnvIds); + + if (requestValidator.GetErrors().Count > 0) { - ActionResult failedAuthorizationResult = await authorization.Authorize(settings.AuthorizedScopePersonal); - if (failedAuthorizationResult != null) - { - return failedAuthorizationResult; - } + return new BadRequestObjectResult(requestValidator.GetErrors()); + } - requestValidator.ValidateQueryParam("env", true, tokenHelper.IsValidEnvironment, out string env); - requestValidator.ValidateQueryParam("scopes", false, tokenHelper.TryParseScopes, out string[] scopes, new[] { "altinn:enduser" }); - requestValidator.ValidateQueryParam("userId", false, uint.TryParse, out uint userId); - requestValidator.ValidateQueryParam("partyId", false, uint.TryParse, out uint partyId); - requestValidator.ValidateQueryParam("pid", false, tokenHelper.IsValidPid, out string pid); - requestValidator.ValidateQueryParam("bulkCount", false, uint.TryParse, out uint bulkCount); - requestValidator.ValidateQueryParam("authLvl", false, tokenHelper.IsValidAuthLvl, out string authLvl, "3"); - requestValidator.ValidateQueryParam("consumerOrgNo", false, tokenHelper.IsValidPidOrOrgNo, out string consumerOrgNo, "991825827"); - requestValidator.ValidateQueryParam("partyuuid", false, Guid.TryParse, out Guid partyUuid); - requestValidator.ValidateQueryParam("userName", false, tokenHelper.IsValidIdentifier, out string userName, ""); - requestValidator.ValidateQueryParam("clientAmr", false, tokenHelper.IsValidIdentifier, out string clientAmr, "virksomhetssertifikat"); - requestValidator.ValidateQueryParam("ttl", false, uint.TryParse, out uint ttl, 1800); - requestValidator.ValidateQueryParam("delegationSource", false, tokenHelper.IsValidUri, out string delegationSource); - - if (requestValidator.GetErrors().Count > 0) + if (bulkCount > 0) + { + var randomList = randomIdentifier.GetRandomPersonalIdentifiers(bulkCount); + var tokenList = await tokenHelper.GetTokenList(randomList, async randomPid => + await tokenHelper.GetPersonalToken(req, env, scopes, userId, partyId, randomPid, authLvl, consumerOrgNo, userName, clientAmr, ttl, delegationSource, partyUuid)); + + return new OkObjectResult(tokenList); + } + + if (getEnvIds) + { + if (!settings.EnvPlatformSubscriptionKeyDict.TryGetValue(env, out string subscriptionKey)) { - return new BadRequestObjectResult(requestValidator.GetErrors()); + return new BadRequestObjectResult($"No subscription key configured for environment: {env}"); } - if (bulkCount > 0) + if (string.IsNullOrWhiteSpace(pid)) { - var randomList = randomIdentifier.GetRandomPersonalIdentifiers(bulkCount); - var tokenList = await tokenHelper.GetTokenList(randomList, async randomPid => - await tokenHelper.GetPersonalToken(req, env, scopes, userId, partyId, randomPid, authLvl, consumerOrgNo, userName, clientAmr, ttl, delegationSource, partyUuid)); - - return new OkObjectResult(tokenList); + return new BadRequestObjectResult("pid is required when getEnvIds is true."); } - - pid ??= randomIdentifier.GetRandomPersonalIdentifiers(1).First(); - string token = await tokenHelper.GetPersonalToken(req, env, scopes, userId, partyId, pid, authLvl, consumerOrgNo, userName, clientAmr, ttl, delegationSource, partyUuid); - if (!string.IsNullOrEmpty(req.Query["dump"])) + var platformAccessToken = await tokenHelper.GetPlatformAccessToken(env, settings.PlatformAccessTokenIssuerName, 300); + var result = await registerService.GetEnvironmentIdentifiers(env, pid, platformAccessToken, subscriptionKey, cancellationSource.Token); + if (!result.Success) { - return new OkObjectResult(tokenHelper.Dump(token)); + return new BadRequestObjectResult("Could not retrieve environment identifiers. Check that the pid is valid for the specified environment."); } - return new OkObjectResult(token); + userId = result.Party.User.UserId; + userName = result.Party.User.Username; + partyId = result.Party.PartyId; + partyUuid = result.Party.Uuid; } + + pid ??= randomIdentifier.GetRandomPersonalIdentifiers(1).First(); + string token = await tokenHelper.GetPersonalToken(req, env, scopes, userId, partyId, pid, authLvl, consumerOrgNo, userName, clientAmr, ttl, delegationSource, partyUuid); + + if (!string.IsNullOrEmpty(req.Query["dump"])) + { + return new OkObjectResult(tokenHelper.Dump(token)); + } + + return new OkObjectResult(token); } } diff --git a/TokenGenerator/Models/Register/ListObject.cs b/TokenGenerator/Models/Register/ListObject.cs new file mode 100644 index 0000000..61ad5b1 --- /dev/null +++ b/TokenGenerator/Models/Register/ListObject.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Altinn.Register.Models; + +/// +/// A list object is a wrapper around a list of items to allow for the API to be +/// extended in the future without breaking backwards compatibility. +/// +public abstract record ListObject +{ + /// + /// Creates a new from a list of items. + /// + /// The list type. + /// The list of items. + /// A . + public static ListObject Create(IEnumerable items) + => new(items); +} + +/// +/// A concrete list object. +/// +/// The item type. +/// The items. +public record ListObject( + [property: JsonPropertyName("data")] + IEnumerable Items) + : ListObject; diff --git a/TokenGenerator/Models/Register/Party.cs b/TokenGenerator/Models/Register/Party.cs new file mode 100644 index 0000000..947ea9d --- /dev/null +++ b/TokenGenerator/Models/Register/Party.cs @@ -0,0 +1,70 @@ +using System; +using System.Text.Json.Serialization; + +namespace Altinn.Register.Models; + +/// +/// Represents a party in Altinn Register. +/// +public class Party +{ + /// + /// Gets the type of the party. + /// + [JsonPropertyName("partyType")] + public string Type { get; } + + /// + /// Gets the type of the party. + /// + [JsonPropertyName("personIdentifier")] + public string Pid { get; } + + /// + /// Gets the UUID of the party. + /// + [JsonPropertyName("partyUuid")] + public Guid Uuid { get; init; } + + /// + /// Gets the canonical URN of the party. + /// + [JsonPropertyName("urn")] + public string Urn { get; init; } + + /// + /// Gets the ID of the party. + /// + [JsonPropertyName("partyId")] + public uint PartyId { get; init; } + + /// + /// Gets the display-name of the party. + /// + [JsonPropertyName("displayName")] + public string DisplayName { get; init; } + + /// + /// Gets the user object associated with the party. + /// + [JsonPropertyName("user")] + public User User { get; init; } +} + +/// +/// Represents the user properties from Altinn Register. +/// +public class User +{ + /// + /// Gets the userId of the party. + /// + [JsonPropertyName("userId")] + public uint UserId { get; } + + /// + /// Gets the username of the party. + /// + [JsonPropertyName("username")] + public string Username { get; } +} \ No newline at end of file diff --git a/TokenGenerator/Services/Interfaces/IRegisterService.cs b/TokenGenerator/Services/Interfaces/IRegisterService.cs new file mode 100644 index 0000000..166040c --- /dev/null +++ b/TokenGenerator/Services/Interfaces/IRegisterService.cs @@ -0,0 +1,11 @@ +using Altinn.Register.Models; +using System.Threading; +using System.Threading.Tasks; + +namespace TokenGenerator.Services.Interfaces +{ + public interface IRegisterService + { + Task<(bool Success, Party Party)> GetEnvironmentIdentifiers(string env, string pid, string platformAccessToken, string subscriptionKey, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/TokenGenerator/Services/RegisterService.cs b/TokenGenerator/Services/RegisterService.cs new file mode 100644 index 0000000..dc55d55 --- /dev/null +++ b/TokenGenerator/Services/RegisterService.cs @@ -0,0 +1,52 @@ +using Altinn.Register.Models; +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using TokenGenerator.Services.Interfaces; + +namespace TokenGenerator.Services; + +public class RegisterService : IRegisterService +{ + private readonly HttpClient httpClient; + + public RegisterService(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task<(bool Success, Party Party)> GetEnvironmentIdentifiers(string env, string pid, string platformAccessToken, string subscriptionKey, CancellationToken cancellationToken) + { + string requestUri = $"https://platform.{env}.altinn.cloud/register/api/v1/access-management/parties/query?fields=user"; + if (env.Equals("tt02", StringComparison.OrdinalIgnoreCase)) + { + requestUri = $"https://platform.tt02.altinn.no/register/api/v1/access-management/parties/query?fields=user"; + } + + ListObject body = ListObject.Create(new[] { pid }); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri) + { + Content = JsonContent.Create(body) + }; + request.Headers.Add("PlatformAccessToken", platformAccessToken); + request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey); + + var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken); + if (response.IsSuccessStatusCode) + { + ListObject result = await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken); + if (result == null || !result.Items.Any()) + { + return (false, null); + } + + return (true, result.Items.First()); + } + + return (false, null); + } +} diff --git a/TokenGenerator/Settings.cs b/TokenGenerator/Settings.cs index 6459df9..0626094 100644 --- a/TokenGenerator/Settings.cs +++ b/TokenGenerator/Settings.cs @@ -28,10 +28,12 @@ public class Settings public string EnvironmentsApiToken { get; set; } public string EnvironmentsConsentToken { get; set; } public string EnvironmentsTtdAccessToken { get; set; } + public string EnvironmentSubscriptionKeys { get; set; } public string IssuerSigningKeys { get; set; } public Dictionary EnvironmentsApiTokenDict => GetKeyValuePairs(EnvironmentsApiToken); public Dictionary EnvironmentsConsentTokenDict => GetKeyValuePairs(EnvironmentsConsentToken); public Dictionary EnvironmentsTtdAccessTokenDict => GetKeyValuePairs(EnvironmentsTtdAccessToken); + public Dictionary EnvPlatformSubscriptionKeyDict => GetKeyValuePairs(EnvironmentSubscriptionKeys); public Dictionary IssuerSigningKeysDict => GetKeyValuePairs(IssuerSigningKeys); private Dictionary GetKeyValuePairs(string stringfieldToSplit, char fieldSeparator = ';', char keyValueSeparator = ':') diff --git a/TokenGenerator/Startup.cs b/TokenGenerator/Startup.cs index 6dfba6a..c7508a2 100644 --- a/TokenGenerator/Startup.cs +++ b/TokenGenerator/Startup.cs @@ -35,6 +35,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddHttpClient(); } } } diff --git a/TokenGenerator/TokenGenerator.csproj b/TokenGenerator/TokenGenerator.csproj index ce6e750..0d4549e 100644 --- a/TokenGenerator/TokenGenerator.csproj +++ b/TokenGenerator/TokenGenerator.csproj @@ -10,6 +10,7 @@ + diff --git a/TokenGenerator/local.settings.json.COPYME b/TokenGenerator/local.settings.json.COPYME index a53ea24..3d8e339 100644 --- a/TokenGenerator/local.settings.json.COPYME +++ b/TokenGenerator/local.settings.json.COPYME @@ -21,6 +21,7 @@ "EnvironmentsApiToken": "none:;dev:altinn-testtools-kv", "EnvironmentsConsentToken": "dev:altinn-testtools-kv", "EnvironmentsTtdAccessToken": "dev:altinn-testtools-kv", + "EnvironmentSubscriptionKeys": "dev:mysubscriptionkey", "TokenAuthorizationWellKnownEndpoint": "https://test.maskinporten.no/.well-known/oauth-authorization-server", "IssuerSigningKeys": "key1:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJjBP7srUVMWuPAzzSZPiPnmpYdsDuQg28p3d4oj9+UahRANCAAQ+0RXI0tdgdkhEVWb/tdkIdstPsMo8JnBCJwmV98/LOAnROt4EAiSeFWTwCtF64wBiDICzAHAiNXxCik6ZL51G" }