Skip to content

Commit

Permalink
Add function GetPlatformAccessToken
Browse files Browse the repository at this point in the history
  • Loading branch information
SandGrainOne committed Oct 30, 2023
1 parent 1198078 commit 6042bba
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 44 deletions.
8 changes: 4 additions & 4 deletions AltinnTestTools.sln
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29920.165
# Visual Studio Version 17
VisualStudioVersion = 17.7.34202.233
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenGenerator", "TokenGenerator\TokenGenerator.csproj", "{51860523-231F-4BCC-97E4-BF41978237ED}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD0CAC07-E747-4A89-A123-C2982D0D48AC}"
ProjectSection(SolutionItems) = preProject
.github\workflows\cd-tokengenerator.yml = .github\workflows\cd-tokengenerator.yml
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TokenGeneratorCli", "TokenGeneratorCli\TokenGeneratorCli.csproj", "{6BABB466-D89B-476E-B18A-27C59819F6DE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenGeneratorCli", "TokenGeneratorCli\TokenGeneratorCli.csproj", "{6BABB466-D89B-476E-B18A-27C59819F6DE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
54 changes: 54 additions & 0 deletions TokenGenerator/GetPlatformAccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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;

namespace TokenGenerator
{
public class GetPlatformAccessToken
{
private readonly IToken tokenHelper;
private readonly IRequestValidator requestValidator;
private readonly IAuthorization authorization;
private readonly Settings settings;

public GetPlatformAccessToken(IToken tokenHelper, IRequestValidator requestValidator, IAuthorization authorization, IOptions<Settings> settings)
{
this.tokenHelper = tokenHelper;
this.requestValidator = requestValidator;
this.authorization = authorization;
this.settings = settings.Value;
}

[FunctionName(nameof(GetPlatformAccessToken))]
public async Task<ActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req)
{
ActionResult failedAuthorizationResult = await authorization.Authorize(settings.AuthorizedScopePlatform);
if (failedAuthorizationResult != null)
{
return failedAuthorizationResult;
}

requestValidator.ValidateQueryParam("env", true, tokenHelper.IsValidEnvironment, out string env);
requestValidator.ValidateQueryParam("app", true, tokenHelper.IsValidDottedIdentifier, out string appClaim);
requestValidator.ValidateQueryParam<uint>("ttl", false, uint.TryParse, out uint ttl, 1800);

if (requestValidator.GetErrors().Count > 0)
{
return new BadRequestObjectResult(requestValidator.GetErrors());
}

string token = await tokenHelper.GetPlatformAccessToken(env, appClaim, ttl);

if (!string.IsNullOrEmpty(req.Query["dump"]))
{
return new OkObjectResult(tokenHelper.Dump(token));
}

return new OkObjectResult(token);
}
}
}
47 changes: 31 additions & 16 deletions TokenGenerator/Services/CertificateKeyVault.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;

using Azure.Identity;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Secrets;

using Microsoft.Extensions.Options;

using TokenGenerator.Services.Interfaces;

namespace TokenGenerator.Services
{
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Azure.Identity;

using Microsoft.Extensions.Options;

public class CertificateKeyVault : ICertificateService
{
private readonly Settings settings;
Expand Down Expand Up @@ -55,26 +56,40 @@ public async Task<X509Certificate2> GetConsentTokenSigningCertificate(string env
return GetLatestCertificateWithRolloverDelay(certificates, 1);
}

public async Task<X509Certificate2> GetPlatformAccessTokenSigningCertificate(string environment)
{
if (string.IsNullOrEmpty(environment) || settings.EnvironmentsApiTokenDict[environment] == null || settings.PlatformAccessTokenSigningCertNamesDict[environment] == null)
{
throw new ArgumentException("Invalid environment");
}

var certificates = await GetCertificates(settings.EnvironmentsApiTokenDict[environment], settings.PlatformAccessTokenSigningCertNamesDict[environment]);

return GetLatestCertificateWithRolloverDelay(certificates, 1);
}

private async Task<List<X509Certificate2>> GetCertificates(string keyVaultName, string certificateName)
{
await _semaphore.WaitAsync();

string cacheKey = string.Concat(keyVaultName, certificateName);

try
{
if (_certificateUpdateTime > DateTime.Now && _certificates.ContainsKey(keyVaultName) &&
_certificates[keyVaultName].Count > 0)
if (_certificateUpdateTime > DateTime.Now && _certificates.ContainsKey(cacheKey) &&
_certificates[cacheKey].Count > 0)
{
return _certificates[keyVaultName];
return _certificates[cacheKey];
}

_certificates[keyVaultName] = await GetAllCertificateVersions(keyVaultName, certificateName);
_certificates[cacheKey] = await GetAllCertificateVersions(keyVaultName, certificateName);

// Reuse the same list of certificates for 1 hour.
_certificateUpdateTime = DateTime.Now.AddHours(1);

_certificates[keyVaultName] =
_certificates[keyVaultName].OrderByDescending(cer => cer.NotBefore).ToList();
return _certificates[keyVaultName];
_certificates[cacheKey] =
_certificates[cacheKey].OrderByDescending(cer => cer.NotBefore).ToList();
return _certificates[cacheKey];
}
finally
{
Expand Down
5 changes: 5 additions & 0 deletions TokenGenerator/Services/CertificatePfx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,10 @@ public async Task<X509Certificate2> GetConsentTokenSigningCertificate(string _)

return await Task.FromResult(consentTokenSigningCertificate);
}

public Task<X509Certificate2> GetPlatformAccessTokenSigningCertificate(string environment)
{
throw new NotImplementedException();
}
}
}
1 change: 1 addition & 0 deletions TokenGenerator/Services/Interfaces/ICertificateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public interface ICertificateService
{
Task<X509Certificate2> GetApiTokenSigningCertificate(string environment);
Task<X509Certificate2> GetConsentTokenSigningCertificate(string environment);
Task<X509Certificate2> GetPlatformAccessTokenSigningCertificate(string environment);
}
}
2 changes: 2 additions & 0 deletions TokenGenerator/Services/Interfaces/IToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface IToken
Task<string> GetPersonalToken(string env, string[] scopes, uint userId, uint partyId, string pid, string authLvl, string consumerOrgNo, string userName, string clientAmr, uint ttl, string delegationSource);
Task<string> GetConsentToken(string env, string[] serviceCodes, IQueryCollection queryParameters, Guid authorizationCode, string offeredBy, string coveredBy, string handledBy, uint ttl);
Task<string> GetPlatformToken(string env, string appClaim, uint ttl);
Task<string> GetPlatformAccessToken(string env, string appClaim, uint ttl);

string Dump(string token);
bool IsValidAuthLvl(string authLvl);
bool IsValidIdentifier(string identifier);
Expand Down
79 changes: 55 additions & 24 deletions TokenGenerator/Services/Token.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

using Newtonsoft.Json;

using TokenGenerator.Services.Interfaces;

namespace TokenGenerator.Services
Expand Down Expand Up @@ -234,30 +238,31 @@ public async Task<string> GetConsentToken(string env, string[] serviceCodes, IQu
return handler.WriteToken(securityToken);
}

/// <summary>
/// Generates a type of AccessToken token and signing it with the same certificate as used by Authentication.
/// </summary>
/// <param name="env">The environment id.</param>
/// <param name="appClaim">The name of the app.</param>
/// <param name="ttl">Time to live.</param>
public async Task<string> GetPlatformToken(string env, string appClaim, uint ttl)
{
var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
string issuer = GetIssuer(env);
var signingCertificate = await certificateHelper.GetApiTokenSigningCertificate(env);
var securityKey = new X509SecurityKey(signingCertificate);
var header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256))
{
{ "x5c", signingCertificate.Thumbprint }
};

var payload = new JwtPayload
{
{ "urn:altinn:app", appClaim },
{ "exp", dateTimeOffset.ToUnixTimeSeconds() + ttl },
{ "iat", dateTimeOffset.ToUnixTimeSeconds() },
{ "iss", GetIssuer(env) },
{ "actual_iss", "altinn-test-tools" },
{ "nbf", dateTimeOffset.ToUnixTimeSeconds() },
};

var securityToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
return CreateAccessToken(appClaim, ttl, issuer, signingCertificate);
}

return handler.WriteToken(securityToken);
/// <summary>
/// Generates a "propper" AccessToken token and signing it with the same platform access token certificate.
/// </summary>
/// <remarks>The issuer is hard coded to <c>platform</c>. The value is used by AccessTokenHandler to identify
/// correct public key when validating the token signature.</remarks>
/// <param name="env">The environment id.</param>
/// <param name="appClaim">The name of the platform application.</param>
/// <param name="ttl">Time to live.</param>
public async Task<string> GetPlatformAccessToken(string env, string appClaim, uint ttl)
{
var signingCertificate = await certificateHelper.GetPlatformAccessTokenSigningCertificate(env);
return CreateAccessToken(appClaim, ttl, "platform", signingCertificate);
}

public bool TryParseScopes(string input, out string[] scopes)
Expand Down Expand Up @@ -346,6 +351,7 @@ public string Dump(string token)
}

private readonly Random random = new Random();

private string RandomString(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
Expand All @@ -363,5 +369,30 @@ private static string GetIssuer(string env)
string tld = env.ToLowerInvariant().StartsWith("at") ? "cloud" : "no";
return $"https://platform.{env}.altinn.{tld}/authentication/api/v1/openid/";
}

private static string CreateAccessToken(string appClaim, uint ttl, string issuer, X509Certificate2 signingCertificate)
{
var securityKey = new X509SecurityKey(signingCertificate);
var header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256))
{
{ "x5c", signingCertificate.Thumbprint }
};

var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
var payload = new JwtPayload
{
{ "urn:altinn:app", appClaim },
{ "exp", dateTimeOffset.ToUnixTimeSeconds() + ttl },
{ "iat", dateTimeOffset.ToUnixTimeSeconds() },
{ "iss", issuer },
{ "actual_iss", "altinn-test-tools" },
{ "nbf", dateTimeOffset.ToUnixTimeSeconds() },
};

var securityToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();

return handler.WriteToken(securityToken);
}
}
}
2 changes: 2 additions & 0 deletions TokenGenerator/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class Settings
{
public string ApiTokenSigningCertNames { get; set; }
public Dictionary<string, string> ApiTokenSigningCertNamesDict => GetKeyValuePairs(ApiTokenSigningCertNames);
public string PlatformAccessTokenSigningCertNames { get; set; }
public Dictionary<string, string> PlatformAccessTokenSigningCertNamesDict => GetKeyValuePairs(PlatformAccessTokenSigningCertNames);
public string ConsentTokenSigningCertNames { get; set; }
public Dictionary<string, string> ConsentTokenSigningCertNamesDict => GetKeyValuePairs(ConsentTokenSigningCertNames);
public string BasicAuthorizationUsers { get; set; }
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 @@ -6,6 +6,7 @@
},
"Settings": {
"ApiTokenSigningCertNames": "dev:altinn-testtools-api-token-signing-cert",
"PlatformAccessTokenSigningCertNames": "dev:altinn-testtools-api-token-signing-cert",
"AuthorizedScope": "altinn:testtools/tokengenerator",
"AuthorizedScopeEnterprise": "altinn:testtools/tokengenerator/enterprise",
"AuthorizedScopeEnterpriseUser": "altinn:testtools/tokengenerator/enterpriseuser",
Expand Down

0 comments on commit 6042bba

Please sign in to comment.