Skip to content

Commit

Permalink
New page for easily finding the Urls for localtest ApiTokens (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarne authored Nov 27, 2024
1 parent 3ad4306 commit d7dc726
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 33 deletions.
2 changes: 1 addition & 1 deletion src/Configuration/LocalPlatformSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public string LocalTestingStaticTestDataPath
}
}

public string LocalFrontendHostname { get; set; }
public string LocalFrontendHostname { get; set; } = "localhost";

public string LocalFrontendProtocol { get; set; } = "http";

Expand Down
60 changes: 48 additions & 12 deletions src/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public async Task<IActionResult> Index()
model.Org = model.TestApps[0].Value?.Split("/").FirstOrDefault();
model.App = model.TestApps[0].Value?.Split("/").LastOrDefault();
}
model.TestUsers = await GetTestUsersForList();
model.TestUsers = await GetTestUsersAndPartiesSelectList();
model.UserSelect = Request.Cookies["Localtest_User.Party_Select"];
var defaultAuthLevel = await GetAppAuthLevel(model.AppModeIsHttp, model.TestApps);
model.AuthenticationLevels = GetAuthenticationLevels(defaultAuthLevel);
Expand Down Expand Up @@ -112,6 +112,7 @@ public IActionResult Error()
/// <summary>
/// Method that logs inn test user
/// </summary>
/// <param name="action">Set to "reauthenticate" if you want to set cookies with no redirect</param>
/// <param name="startAppModel">An object with information about app and user.</param>
/// <returns>Redirects to returnUrl</returns>
[HttpPost]
Expand Down Expand Up @@ -175,13 +176,28 @@ public async Task<ActionResult> LogInTestUser(string action, StartAppModel start
return Redirect($"/{app.Id}/");
}

[HttpGet("/Home/Tokens")]
public async Task<IActionResult> Tokens()
{
var model = new TokensViewModel
{
AuthenticationLevels = GetAuthenticationLevels(2),
TestUsers = await GetUsersSelectList(),
DefaultOrg = _localPlatformSettings.LocalAppMode == "http" ? (await GetAppsList()).First().Value?.Split("/").FirstOrDefault() : null,
};

return View(model);
}


/// <summary>
///
/// Returns a user token with the given userId as claim
/// </summary>
/// <param name="userId"></param>
/// <param name="userId">UserId of the token holder</param>
/// <param name="authenticationLevel">Authentication level of the token</param>
/// <returns></returns>
[HttpGet("{userId}")]
public async Task<ActionResult> GetTestUserToken(int userId)
[HttpGet("/Home/GetTestUserToken/{userId?}")]
public async Task<ActionResult> GetTestUserToken(int userId, [FromQuery] int authenticationLevel = 2)
{
UserProfile profile = await _userProfileService.GetUser(userId);

Expand All @@ -191,25 +207,28 @@ public async Task<ActionResult> GetTestUserToken(int userId)
}

// Create a test token with long duration
string token = await _authenticationService.GenerateTokenForProfile(profile, 2);
string token = await _authenticationService.GenerateTokenForProfile(profile, authenticationLevel);
return Ok(token);
}

/// <summary>
/// Returns a org token with the given org as claim
/// </summary>
/// <param name="id"></param>
/// <param name="org">The short code used to identify the service owner org</param>
/// <param name="orgNumber">Organization number to be included in token (if not an official service owner)</param>
/// <param name="authenticationLevel">Authentication level of the token</param>
/// <returns></returns>
[HttpGet("{id}")]
public async Task<ActionResult> GetTestOrgToken(string id, [FromQuery] string orgNumber = null, [FromQuery] string scopes = null)
[HttpGet("/Home/GetTestOrgToken/{org?}")]
public async Task<ActionResult> GetTestOrgToken(string org, [FromQuery] string orgNumber = null, [FromQuery] string scopes = null, [FromQuery] int? authenticationLevel = 3)
{

// Create a test token with long duration
string token = await _authenticationService.GenerateTokenForOrg(id, orgNumber, scopes);
string token = await _authenticationService.GenerateTokenForOrg(org, orgNumber, scopes, authenticationLevel);

return Ok(token);
}

private async Task<IEnumerable<SelectListItem>> GetTestUsersForList()
private async Task<IEnumerable<SelectListItem>> GetTestUsersAndPartiesSelectList()
{
var data = await _testDataService.GetTestData();
var userItems = new List<SelectListItem>();
Expand Down Expand Up @@ -252,6 +271,23 @@ private async Task<IEnumerable<SelectListItem>> GetTestUsersForList()
return userItems;
}

private async Task<List<SelectListItem>> GetUsersSelectList()
{
var data = await _testDataService.GetTestData();
var testUsers = new List<SelectListItem>();
foreach (UserProfile profile in data.Profile.User.Values)
{
var properProfile = await _userProfileService.GetUser(profile.UserId);
testUsers.Add(new()
{
Text = properProfile?.Party.Name,
Value = profile.UserId.ToString(),
});
}

return testUsers;
}

private async Task<int> GetAppAuthLevel(bool isHttp, IEnumerable<SelectListItem> testApps)
{
if (!isHttp)
Expand All @@ -276,7 +312,7 @@ private async Task<int> GetAppAuthLevel(bool isHttp, IEnumerable<SelectListItem>
}
}

private List<SelectListItem> GetAuthenticationLevels(int defaultAuthLevel)
private static List<SelectListItem> GetAuthenticationLevels(int defaultAuthLevel)
{
return new()
{
Expand Down
11 changes: 11 additions & 0 deletions src/Models/TokensModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable
using Microsoft.AspNetCore.Mvc.Rendering;

namespace LocalTest.Models;

public class TokensViewModel
{
public required IEnumerable<SelectListItem> TestUsers { get; init; }
public required List<SelectListItem> AuthenticationLevels { get; init; }
public required string DefaultOrg { get; init; }
}
93 changes: 74 additions & 19 deletions src/Services/Authentication/Implementation/AuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using AuthSettings = Altinn.Platform.Authentication.Configuration.GeneralSettings;
using Altinn.Platform.Authorization.Services.Interface;
using Altinn.Platform.Profile.Models;
using AltinnCore.Authentication.Constants;
using LocalTest.Clients.CdnAltinnOrgs;
using LocalTest.Configuration;
using LocalTest.Services.Authentication.Interface;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using LocalTest.Configuration;
using AltinnCore.Authentication.Constants;
using Altinn.Platform.Authorization.Services.Interface;
using AuthSettings = Altinn.Platform.Authentication.Configuration.GeneralSettings;

namespace LocalTest.Services.Authentication.Implementation;

Expand All @@ -24,13 +21,19 @@ public class AuthenticationService : IAuthentication
private readonly GeneralSettings _generalSettings;
private readonly IClaims _claimsService;

public AuthenticationService(AltinnOrgsClient orgsClient, IOptions<AuthSettings> authSettings, IOptions<GeneralSettings> generalSettings, IClaims claimsService)
public AuthenticationService(
AltinnOrgsClient orgsClient,
IOptions<AuthSettings> authSettings,
IOptions<GeneralSettings> generalSettings,
IClaims claimsService
)
{
_orgsClient = orgsClient;
_authSettings = authSettings.Value;
_generalSettings = generalSettings.Value;
_claimsService = claimsService;
}

///<inheritdoc/>
public string GenerateToken(ClaimsPrincipal principal)
{
Expand All @@ -56,7 +59,12 @@ public string GenerateToken(ClaimsPrincipal principal)
}

/// <inheritdoc />
public async Task<string> GenerateTokenForOrg(string org, string? orgNumber = null, string? scopes = null)
public async Task<string> GenerateTokenForOrg(
string org,
string? orgNumber = null,
string? scopes = null,
int? authenticationLevel = null
)
{
if (orgNumber is null)
{
Expand All @@ -66,14 +74,27 @@ public async Task<string> GenerateTokenForOrg(string org, string? orgNumber = nu

List<Claim> claims = new List<Claim>();
string issuer = _generalSettings.Hostname;
claims.Add(new Claim(AltinnCoreClaimTypes.Org, org.ToLower(), ClaimValueTypes.String, issuer));
// 3 is the default level for altinn tokens form Maskinporten
claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, "3", ClaimValueTypes.Integer32, issuer));
claims.Add(
new Claim(AltinnCoreClaimTypes.Org, org.ToLower(), ClaimValueTypes.String, issuer)
);
claims.Add(
new Claim(
AltinnCoreClaimTypes.AuthenticationLevel,
// 3 is the default authentication level from maskinporten
(authenticationLevel ?? 3).ToString(),
ClaimValueTypes.Integer32,
issuer
)
);

scopes ??= "altinn:serviceowner/instances.read";
claims.Add(new Claim("urn:altinn:scope", scopes, ClaimValueTypes.String, issuer));

if (!string.IsNullOrEmpty(orgNumber))
{
claims.Add(new Claim(AltinnCoreClaimTypes.OrgNumber, orgNumber, ClaimValueTypes.String, issuer));
claims.Add(
new Claim(AltinnCoreClaimTypes.OrgNumber, orgNumber, ClaimValueTypes.String, issuer)
);
}

ClaimsIdentity identity = new ClaimsIdentity(_generalSettings.GetClaimsIdentity);
Expand All @@ -89,11 +110,46 @@ public async Task<string> GenerateTokenForProfile(UserProfile profile, int authe
{
List<Claim> claims = new List<Claim>();
string issuer = _generalSettings.Hostname;
claims.Add(new Claim(ClaimTypes.NameIdentifier, profile.UserId.ToString(), ClaimValueTypes.String, issuer));
claims.Add(new Claim(AltinnCoreClaimTypes.UserId, profile.UserId.ToString(), ClaimValueTypes.String, issuer));
claims.Add(new Claim(AltinnCoreClaimTypes.UserName, profile.UserName, ClaimValueTypes.String, issuer));
claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, profile.PartyId.ToString(), ClaimValueTypes.Integer32, issuer));
claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer));
claims.Add(
new Claim(
ClaimTypes.NameIdentifier,
profile.UserId.ToString(),
ClaimValueTypes.String,
issuer
)
);
claims.Add(
new Claim(
AltinnCoreClaimTypes.UserId,
profile.UserId.ToString(),
ClaimValueTypes.String,
issuer
)
);
claims.Add(
new Claim(
AltinnCoreClaimTypes.UserName,
profile.UserName,
ClaimValueTypes.String,
issuer
)
);
claims.Add(
new Claim(
AltinnCoreClaimTypes.PartyID,
profile.PartyId.ToString(),
ClaimValueTypes.Integer32,
issuer
)
);
claims.Add(
new Claim(
AltinnCoreClaimTypes.AuthenticationLevel,
authenticationLevel.ToString(),
ClaimValueTypes.Integer32,
issuer
)
);
claims.AddRange(await _claimsService.GetCustomClaims(profile.UserId, issuer));

ClaimsIdentity identity = new ClaimsIdentity(_generalSettings.GetClaimsIdentity);
Expand All @@ -103,4 +159,3 @@ public async Task<string> GenerateTokenForProfile(UserProfile profile, int authe
return GenerateToken(principal);
}
}

9 changes: 8 additions & 1 deletion src/Services/Authentication/Interface/IAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ public interface IAuthentication
/// </summary>
/// <param name="org">Three letter application owner name (eg, TST )</param>
/// <param name="orgNumber">Optional Organization number for the application owner. Will be fetched if not provided</param>
/// <param name="scopes">Space separated scopes for the token. If null default to "altinn:serviceowner/instances.read"</param>
/// <param name="authenticationLevel">The authentication level of the generated token</param>
/// <returns>JWT token</returns>
public Task<string> GenerateTokenForOrg(string org, string? orgNumber = null, string? scopes = null);
public Task<string> GenerateTokenForOrg(
string org,
string? orgNumber = null,
string? scopes = null,
int? authenticationLevel = null
);

/// <summary>
/// Get JWT token for user profile
Expand Down
71 changes: 71 additions & 0 deletions src/Views/Home/Tokens.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
@model TokensViewModel
@{
ViewData["Title"] = "Tokens for localtest";
}
<div class="container">
<h1 class="display-4">Welcome to Altinn App Local Testing</h1>
<p>Create tokens for accessing the localtest apis.</p>

<div class="alert alert-warning">Note that LocalTest is not an exact replica of the production systems, and that there are differences</div>

<div class="flex flex-column">

<h2>Generate end users token (like from idporten)</h2>
@using (Html.BeginForm("GetTestUserToken", "Home", FormMethod.Get, new { Class = "form" }))
{
<div class="form-group">
<label>Select user</label>
@Html.DropDownList("userId", Model.TestUsers, new { Class = "form-control" })
</div>
<div class="form-group">
<label>Select your authentication level</label>
@Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" })
</div>

<div class="form-group">
<button type="submit" class="btn btn-primary">Get User token</button>
</div>
}
</div>
<div class="flex flex-column">
<h2>Service owner tokens</h2>
@using (Html.BeginForm("GetTestOrgToken", "Home", FormMethod.Get, new { Class = "form-signin" }))
{
<div class="form-group">
<label>Select service owner org</label>
@Html.TextBox("org", Model.DefaultOrg, new { Class = "form-control" })
</div>
<div class="form-group">
<label>Select your authentication level</label>
@Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" })
</div>
<div class="form-group">
<label>optional org number for the token</label>
@Html.TextBox("orgNumber", "", new { Class = "form-control", Placeholder = "For official orgs this is fetch from altinncdn.no" })
</div>
<div class="form-group">
<label>Scopes for the token separated by space " " (scopes does not seem to be verified by localtest storage)</label>

@Html.TextBox("scopes", "", new { Class = "form-control", Id = "scopes", PlaceHolder = "altinn:serviceowner/instances.read" })
</div>

<div class="form-group">
<button type="submit" class="btn btn-primary">Get Org token</button>
</div>
}
</div>
</div>

@section Scripts
{
<script>
document.getElementByid("addScopeBtn")
</script>
}

@section Styles
{
<style>
</style>
}
5 changes: 5 additions & 0 deletions src/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<title>@ViewData["Title"] - Altinn Studio</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="~/localtestresources/css/site.css" />
@RenderSection("Styles", required: false)
</head>
<body>
<header>
Expand All @@ -29,6 +30,10 @@
<a class="nav-link text-secondary" asp-area="" asp-controller="TenorUsers"
asp-action="Index">User administration</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home"
asp-action="Tokens">Tokens</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="StorageExplorer" asp-action="Index">Storage Explorer</a>
</li>
Expand Down

0 comments on commit d7dc726

Please sign in to comment.