diff --git a/src/Configuration/LocalPlatformSettings.cs b/src/Configuration/LocalPlatformSettings.cs index f0e17bf0..d2c0ea83 100644 --- a/src/Configuration/LocalPlatformSettings.cs +++ b/src/Configuration/LocalPlatformSettings.cs @@ -41,7 +41,7 @@ public string LocalTestingStaticTestDataPath } } - public string LocalFrontendHostname { get; set; } + public string LocalFrontendHostname { get; set; } = "localhost"; public string LocalFrontendProtocol { get; set; } = "http"; diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index f63a89e4..dd7a1c8a 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -79,7 +79,7 @@ public async Task 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); @@ -112,6 +112,7 @@ public IActionResult Error() /// /// Method that logs inn test user /// + /// Set to "reauthenticate" if you want to set cookies with no redirect /// An object with information about app and user. /// Redirects to returnUrl [HttpPost] @@ -175,13 +176,28 @@ public async Task LogInTestUser(string action, StartAppModel start return Redirect($"/{app.Id}/"); } + [HttpGet("/Home/Tokens")] + public async Task 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); + } + + /// - /// + /// Returns a user token with the given userId as claim /// - /// + /// UserId of the token holder + /// Authentication level of the token /// - [HttpGet("{userId}")] - public async Task GetTestUserToken(int userId) + [HttpGet("/Home/GetTestUserToken/{userId?}")] + public async Task GetTestUserToken(int userId, [FromQuery] int authenticationLevel = 2) { UserProfile profile = await _userProfileService.GetUser(userId); @@ -191,25 +207,28 @@ public async Task 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); } /// /// Returns a org token with the given org as claim /// - /// + /// The short code used to identify the service owner org + /// Organization number to be included in token (if not an official service owner) + /// Authentication level of the token /// - [HttpGet("{id}")] - public async Task GetTestOrgToken(string id, [FromQuery] string orgNumber = null, [FromQuery] string scopes = null) + [HttpGet("/Home/GetTestOrgToken/{org?}")] + public async Task 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> GetTestUsersForList() + private async Task> GetTestUsersAndPartiesSelectList() { var data = await _testDataService.GetTestData(); var userItems = new List(); @@ -252,6 +271,23 @@ private async Task> GetTestUsersForList() return userItems; } + private async Task> GetUsersSelectList() + { + var data = await _testDataService.GetTestData(); + var testUsers = new List(); + 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 GetAppAuthLevel(bool isHttp, IEnumerable testApps) { if (!isHttp) @@ -276,7 +312,7 @@ private async Task GetAppAuthLevel(bool isHttp, IEnumerable } } - private List GetAuthenticationLevels(int defaultAuthLevel) + private static List GetAuthenticationLevels(int defaultAuthLevel) { return new() { diff --git a/src/Models/TokensModel.cs b/src/Models/TokensModel.cs new file mode 100644 index 00000000..4cb44605 --- /dev/null +++ b/src/Models/TokensModel.cs @@ -0,0 +1,11 @@ +#nullable enable +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace LocalTest.Models; + +public class TokensViewModel +{ + public required IEnumerable TestUsers { get; init; } + public required List AuthenticationLevels { get; init; } + public required string DefaultOrg { get; init; } +} \ No newline at end of file diff --git a/src/Services/Authentication/Implementation/AuthenticationService.cs b/src/Services/Authentication/Implementation/AuthenticationService.cs index f6be90e6..f8890b27 100644 --- a/src/Services/Authentication/Implementation/AuthenticationService.cs +++ b/src/Services/Authentication/Implementation/AuthenticationService.cs @@ -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; @@ -24,13 +21,19 @@ public class AuthenticationService : IAuthentication private readonly GeneralSettings _generalSettings; private readonly IClaims _claimsService; - public AuthenticationService(AltinnOrgsClient orgsClient, IOptions authSettings, IOptions generalSettings, IClaims claimsService) + public AuthenticationService( + AltinnOrgsClient orgsClient, + IOptions authSettings, + IOptions generalSettings, + IClaims claimsService + ) { _orgsClient = orgsClient; _authSettings = authSettings.Value; _generalSettings = generalSettings.Value; _claimsService = claimsService; } + /// public string GenerateToken(ClaimsPrincipal principal) { @@ -56,7 +59,12 @@ public string GenerateToken(ClaimsPrincipal principal) } /// - public async Task GenerateTokenForOrg(string org, string? orgNumber = null, string? scopes = null) + public async Task GenerateTokenForOrg( + string org, + string? orgNumber = null, + string? scopes = null, + int? authenticationLevel = null + ) { if (orgNumber is null) { @@ -66,14 +74,27 @@ public async Task GenerateTokenForOrg(string org, string? orgNumber = nu List claims = new List(); 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); @@ -89,11 +110,46 @@ public async Task GenerateTokenForProfile(UserProfile profile, int authe { List claims = new List(); 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); @@ -103,4 +159,3 @@ public async Task GenerateTokenForProfile(UserProfile profile, int authe return GenerateToken(principal); } } - diff --git a/src/Services/Authentication/Interface/IAuthentication.cs b/src/Services/Authentication/Interface/IAuthentication.cs index f060e87c..e085a5c3 100644 --- a/src/Services/Authentication/Interface/IAuthentication.cs +++ b/src/Services/Authentication/Interface/IAuthentication.cs @@ -19,8 +19,15 @@ public interface IAuthentication /// /// Three letter application owner name (eg, TST ) /// Optional Organization number for the application owner. Will be fetched if not provided + /// Space separated scopes for the token. If null default to "altinn:serviceowner/instances.read" + /// The authentication level of the generated token /// JWT token - public Task GenerateTokenForOrg(string org, string? orgNumber = null, string? scopes = null); + public Task GenerateTokenForOrg( + string org, + string? orgNumber = null, + string? scopes = null, + int? authenticationLevel = null + ); /// /// Get JWT token for user profile diff --git a/src/Views/Home/Tokens.cshtml b/src/Views/Home/Tokens.cshtml new file mode 100644 index 00000000..bdedf161 --- /dev/null +++ b/src/Views/Home/Tokens.cshtml @@ -0,0 +1,71 @@ +@model TokensViewModel +@{ + ViewData["Title"] = "Tokens for localtest"; +} +
+

Welcome to Altinn App Local Testing

+

Create tokens for accessing the localtest apis.

+ +
Note that LocalTest is not an exact replica of the production systems, and that there are differences
+ +
+ +

Generate end users token (like from idporten)

+ @using (Html.BeginForm("GetTestUserToken", "Home", FormMethod.Get, new { Class = "form" })) + { +
+ + @Html.DropDownList("userId", Model.TestUsers, new { Class = "form-control" }) +
+
+ + @Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" }) +
+ +
+ +
+ } +
+
+

Service owner tokens

+ @using (Html.BeginForm("GetTestOrgToken", "Home", FormMethod.Get, new { Class = "form-signin" })) + { +
+ + @Html.TextBox("org", Model.DefaultOrg, new { Class = "form-control" }) +
+
+ + @Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" }) +
+
+ + @Html.TextBox("orgNumber", "", new { Class = "form-control", Placeholder = "For official orgs this is fetch from altinncdn.no" }) +
+
+ + + @Html.TextBox("scopes", "", new { Class = "form-control", Id = "scopes", PlaceHolder = "altinn:serviceowner/instances.read" }) +
+ +
+ +
+ } +
+
+ +@section Scripts +{ + +} + +@section Styles +{ + +} \ No newline at end of file diff --git a/src/Views/Shared/_Layout.cshtml b/src/Views/Shared/_Layout.cshtml index 063cfb44..79e7b8db 100644 --- a/src/Views/Shared/_Layout.cshtml +++ b/src/Views/Shared/_Layout.cshtml @@ -6,6 +6,7 @@ @ViewData["Title"] - Altinn Studio + @RenderSection("Styles", required: false)
@@ -29,6 +30,10 @@ User administration +