From 6042bbacd93a643800bcf0ef7634cc7bb4e316c7 Mon Sep 17 00:00:00 2001 From: Terje Holene Date: Mon, 30 Oct 2023 10:35:59 +0100 Subject: [PATCH] Add function GetPlatformAccessToken --- AltinnTestTools.sln | 8 +- TokenGenerator/GetPlatformAccessToken.cs | 54 +++++++++++++ .../Services/CertificateKeyVault.cs | 47 +++++++---- TokenGenerator/Services/CertificatePfx.cs | 5 ++ .../Interfaces/ICertificateService.cs | 1 + TokenGenerator/Services/Interfaces/IToken.cs | 2 + TokenGenerator/Services/Token.cs | 79 +++++++++++++------ TokenGenerator/Settings.cs | 2 + TokenGenerator/local.settings.json.COPYME | 1 + 9 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 TokenGenerator/GetPlatformAccessToken.cs diff --git a/AltinnTestTools.sln b/AltinnTestTools.sln index 30fef61..ae20937 100644 --- a/AltinnTestTools.sln +++ b/AltinnTestTools.sln @@ -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 diff --git a/TokenGenerator/GetPlatformAccessToken.cs b/TokenGenerator/GetPlatformAccessToken.cs new file mode 100644 index 0000000..178c495 --- /dev/null +++ b/TokenGenerator/GetPlatformAccessToken.cs @@ -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) + { + this.tokenHelper = tokenHelper; + this.requestValidator = requestValidator; + this.authorization = authorization; + this.settings = settings.Value; + } + + [FunctionName(nameof(GetPlatformAccessToken))] + public async Task 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("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); + } + } +} diff --git a/TokenGenerator/Services/CertificateKeyVault.cs b/TokenGenerator/Services/CertificateKeyVault.cs index 02823ad..82084fd 100644 --- a/TokenGenerator/Services/CertificateKeyVault.cs +++ b/TokenGenerator/Services/CertificateKeyVault.cs @@ -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; @@ -55,26 +56,40 @@ public async Task GetConsentTokenSigningCertificate(string env return GetLatestCertificateWithRolloverDelay(certificates, 1); } + public async Task 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> 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 { diff --git a/TokenGenerator/Services/CertificatePfx.cs b/TokenGenerator/Services/CertificatePfx.cs index 12795c2..67e9f79 100644 --- a/TokenGenerator/Services/CertificatePfx.cs +++ b/TokenGenerator/Services/CertificatePfx.cs @@ -52,5 +52,10 @@ public async Task GetConsentTokenSigningCertificate(string _) return await Task.FromResult(consentTokenSigningCertificate); } + + public Task GetPlatformAccessTokenSigningCertificate(string environment) + { + throw new NotImplementedException(); + } } } diff --git a/TokenGenerator/Services/Interfaces/ICertificateService.cs b/TokenGenerator/Services/Interfaces/ICertificateService.cs index 6692537..0f8b501 100644 --- a/TokenGenerator/Services/Interfaces/ICertificateService.cs +++ b/TokenGenerator/Services/Interfaces/ICertificateService.cs @@ -7,5 +7,6 @@ public interface ICertificateService { Task GetApiTokenSigningCertificate(string environment); Task GetConsentTokenSigningCertificate(string environment); + Task GetPlatformAccessTokenSigningCertificate(string environment); } } \ No newline at end of file diff --git a/TokenGenerator/Services/Interfaces/IToken.cs b/TokenGenerator/Services/Interfaces/IToken.cs index 59ee6b5..3bae06f 100644 --- a/TokenGenerator/Services/Interfaces/IToken.cs +++ b/TokenGenerator/Services/Interfaces/IToken.cs @@ -11,6 +11,8 @@ public interface IToken Task GetPersonalToken(string env, string[] scopes, uint userId, uint partyId, string pid, string authLvl, string consumerOrgNo, string userName, string clientAmr, uint ttl, string delegationSource); Task GetConsentToken(string env, string[] serviceCodes, IQueryCollection queryParameters, Guid authorizationCode, string offeredBy, string coveredBy, string handledBy, uint ttl); Task GetPlatformToken(string env, string appClaim, uint ttl); + Task GetPlatformAccessToken(string env, string appClaim, uint ttl); + string Dump(string token); bool IsValidAuthLvl(string authLvl); bool IsValidIdentifier(string identifier); diff --git a/TokenGenerator/Services/Token.cs b/TokenGenerator/Services/Token.cs index 3a9b1dd..632b8bf 100644 --- a/TokenGenerator/Services/Token.cs +++ b/TokenGenerator/Services/Token.cs @@ -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 @@ -234,30 +238,31 @@ public async Task GetConsentToken(string env, string[] serviceCodes, IQu return handler.WriteToken(securityToken); } + /// + /// Generates a type of AccessToken token and signing it with the same certificate as used by Authentication. + /// + /// The environment id. + /// The name of the app. + /// Time to live. public async Task 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); + /// + /// Generates a "propper" AccessToken token and signing it with the same platform access token certificate. + /// + /// The issuer is hard coded to platform. The value is used by AccessTokenHandler to identify + /// correct public key when validating the token signature. + /// The environment id. + /// The name of the platform application. + /// Time to live. + public async Task 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) @@ -346,6 +351,7 @@ public string Dump(string token) } private readonly Random random = new Random(); + private string RandomString(int length) { const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"; @@ -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); + } } } diff --git a/TokenGenerator/Settings.cs b/TokenGenerator/Settings.cs index cfc940e..817023d 100644 --- a/TokenGenerator/Settings.cs +++ b/TokenGenerator/Settings.cs @@ -8,6 +8,8 @@ public class Settings { public string ApiTokenSigningCertNames { get; set; } public Dictionary ApiTokenSigningCertNamesDict => GetKeyValuePairs(ApiTokenSigningCertNames); + public string PlatformAccessTokenSigningCertNames { get; set; } + public Dictionary PlatformAccessTokenSigningCertNamesDict => GetKeyValuePairs(PlatformAccessTokenSigningCertNames); public string ConsentTokenSigningCertNames { get; set; } public Dictionary ConsentTokenSigningCertNamesDict => GetKeyValuePairs(ConsentTokenSigningCertNames); public string BasicAuthorizationUsers { get; set; } diff --git a/TokenGenerator/local.settings.json.COPYME b/TokenGenerator/local.settings.json.COPYME index 99def78..fbd5d85 100644 --- a/TokenGenerator/local.settings.json.COPYME +++ b/TokenGenerator/local.settings.json.COPYME @@ -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",