Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 77 additions & 50 deletions TokenGenerator/GetPersonalToken.cs
Original file line number Diff line number Diff line change
@@ -1,80 +1,107 @@
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;
using Microsoft.AspNetCore.Http;
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> 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> settings)
[FunctionName(nameof(GetPersonalToken))]
public async Task<ActionResult> 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<ActionResult> 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<uint>("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<uint>("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);
}
}
32 changes: 32 additions & 0 deletions TokenGenerator/Models/Register/ListObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#nullable enable

using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Altinn.Register.Models;

/// <summary>
/// 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.
/// </summary>
public abstract record ListObject
{
/// <summary>
/// Creates a new <see cref="ListObject{T}"/> from a list of items.
/// </summary>
/// <typeparam name="T">The list type.</typeparam>
/// <param name="items">The list of items.</param>
/// <returns>A <see cref="ListObject{T}"/>.</returns>
public static ListObject<T> Create<T>(IEnumerable<T> items)
=> new(items);
}

/// <summary>
/// A concrete list object.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="Items">The items.</param>
public record ListObject<T>(
[property: JsonPropertyName("data")]
IEnumerable<T> Items)
: ListObject;
70 changes: 70 additions & 0 deletions TokenGenerator/Models/Register/Party.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Text.Json.Serialization;

namespace Altinn.Register.Models;

/// <summary>
/// Represents a party in Altinn Register.
/// </summary>
public class Party
{
/// <summary>
/// Gets the type of the party.
/// </summary>
[JsonPropertyName("partyType")]
public string Type { get; }

/// <summary>
/// Gets the type of the party.
/// </summary>
[JsonPropertyName("personIdentifier")]
public string Pid { get; }

/// <summary>
/// Gets the UUID of the party.
/// </summary>
[JsonPropertyName("partyUuid")]
public Guid Uuid { get; init; }

/// <summary>
/// Gets the canonical URN of the party.
/// </summary>
[JsonPropertyName("urn")]
public string Urn { get; init; }

/// <summary>
/// Gets the ID of the party.
/// </summary>
[JsonPropertyName("partyId")]
public uint PartyId { get; init; }

/// <summary>
/// Gets the display-name of the party.
/// </summary>
[JsonPropertyName("displayName")]
public string DisplayName { get; init; }

/// <summary>
/// Gets the user object associated with the party.
/// </summary>
[JsonPropertyName("user")]
public User User { get; init; }
}

/// <summary>
/// Represents the user properties from Altinn Register.
/// </summary>
public class User
{
/// <summary>
/// Gets the userId of the party.
/// </summary>
[JsonPropertyName("userId")]
public uint UserId { get; }

/// <summary>
/// Gets the username of the party.
/// </summary>
[JsonPropertyName("username")]
public string Username { get; }
}
11 changes: 11 additions & 0 deletions TokenGenerator/Services/Interfaces/IRegisterService.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
52 changes: 52 additions & 0 deletions TokenGenerator/Services/RegisterService.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<Party> result = await response.Content.ReadFromJsonAsync<ListObject<Party>>(cancellationToken: cancellationToken);
if (result == null || !result.Items.Any())
{
return (false, null);
}

return (true, result.Items.First());
}

return (false, null);
}
}
2 changes: 2 additions & 0 deletions TokenGenerator/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> EnvironmentsApiTokenDict => GetKeyValuePairs(EnvironmentsApiToken);
public Dictionary<string, string> EnvironmentsConsentTokenDict => GetKeyValuePairs(EnvironmentsConsentToken);
public Dictionary<string, string> EnvironmentsTtdAccessTokenDict => GetKeyValuePairs(EnvironmentsTtdAccessToken);
public Dictionary<string, string> EnvPlatformSubscriptionKeyDict => GetKeyValuePairs(EnvironmentSubscriptionKeys);
public Dictionary<string, string> IssuerSigningKeysDict => GetKeyValuePairs(IssuerSigningKeys);

private Dictionary<string, string> GetKeyValuePairs(string stringfieldToSplit, char fieldSeparator = ';', char keyValueSeparator = ':')
Expand Down
1 change: 1 addition & 0 deletions TokenGenerator/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public override void Configure(IFunctionsHostBuilder builder)
builder.Services.AddScoped<IAuthorizationBasic, AuthorizationBasic>();
builder.Services.AddScoped<IRequestValidator, RequestValidator>();
builder.Services.AddScoped<IAuthorization, Authorization>();
builder.Services.AddHttpClient<IRegisterService, RegisterService>();
}
}
}
1 change: 1 addition & 0 deletions TokenGenerator/TokenGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.1" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.1.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.1.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.4.1" />
Expand Down
1 change: 1 addition & 0 deletions TokenGenerator/local.settings.json.COPYME
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down