Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maskinporten auth #210

Merged
merged 26 commits into from
Dec 4, 2023
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
115 changes: 71 additions & 44 deletions Altinn3 Broker.postman_collection_examples.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"info": {
"_postman_id": "d12c336b-b0ca-4614-b9e1-8f4cb6c244fd",
"_postman_id": "59aadcbf-a305-40e5-ae58-5e8635b2cdb5",
"name": "Altinn.Broker",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "7106668"
},
"item": [
@@ -27,6 +27,16 @@
{
"name": "/broker/api/v1/file/:fileId/upload",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{sender_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
@@ -86,6 +96,16 @@
{
"name": "/broker/api/v1/file/:fileId/details",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{sender_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
@@ -148,6 +168,16 @@
{
"name": "/broker/api/v1/file/:fileId/download",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{recipient_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": "{{baseUrl}}/broker/api/v1/file/{{fileId}}/download"
@@ -232,9 +262,13 @@
"request": {
"auth": {
"type": "bearer",
"bearer": {
"token": "{{recipientToken}}"
}
"bearer": [
{
"key": "token",
"value": "{{recipient_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
@@ -280,6 +314,16 @@
{
"name": "/broker/api/v1/file/:fileId",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{sender_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
@@ -354,6 +398,16 @@
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{sender_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
@@ -484,6 +538,16 @@
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{sender_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [
{
@@ -510,7 +574,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"filename\": \"system.ini\",\n \"sendersFileReference\": \"archiveno-20425\",\n \"checksum\": \"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e\",\n \"sender\": \"931669400\",\n \"recipients\": [\n \"924806176\"\n ],\n \"metadata\": {\n \"deseruntde\": \"edtnuresed\",\n \"veniam_1d\": \"d1_mainev\",\n \"dialogId\": \"a15af443-8de8-4ebd-9708-08d407b5bb6c\"\n }\n}",
"raw": "{\n \"filename\": \"system.ini\",\n \"sendersFileReference\": \"archiveno-20425\",\n \"checksum\": \"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e\",\n \"sender\": \"0192:991825827\",\n \"recipients\": [\n \"0192:986252932\"\n ],\n \"metadata\": {\n \"deseruntde\": \"edtnuresed\",\n \"veniam_1d\": \"d1_mainev\",\n \"dialogId\": \"a15af443-8de8-4ebd-9708-08d407b5bb6c\"\n }\n}",
"options": {
"raw": {
"headerFamily": "json",
@@ -607,42 +671,10 @@
]
}
]
},
{
"name": "health",
"item": [
{
"name": "/health",
"request": {
"method": "GET",
"header": [],
"url": "{{baseUrl}}/health"
},
"response": [
{
"name": "Success",
"originalRequest": {
"method": "GET",
"header": [],
"url": "{{baseUrl}}/health"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "text",
"header": [],
"cookie": [],
"body": ""
}
]
}
]
}
],
"auth": {
"type": "bearer",
"bearer": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTkyOjk5MTgyNTgyNyJ9.exFSD-mL1fzoWhg8IKcVeCeEyJ5qpABPU9A1AXHDa_k"
}
"type": "inherit"
},
"event": [
{
@@ -672,11 +704,6 @@
{
"key": "fileId",
"value": ""
},
{
"key": "recipientToken",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTkyOjkyNDgwNjE3NiIsImlhdCI6MTUxNjIzOTAyMn0.aIKdAanTHukndn5D2N3yWF1e2ORhNv6johkbwlGDmDw",
"type": "string"
}
]
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -26,7 +26,11 @@ If you need to re-initialize the database during local development, you can dele

### Authorization

For the time being, we have not implemented proper authorization. Any JWT token with a "sub" claim can be used (see jwt.io to make one). There is a dummy token in the Postman collection that can be used.
To get access to the Broker API, a consumer needs to use a Maskinporten token. Recipients should use the scope altinn:broker.read and senders should use the scope altinn:broker.write. Tokens with both scopes also work. You can create a Maskinporten integration here:
https://selvbetjening-samarbeid-ver2.difi.no/integrations

For more on Maskinporten tokens see:
https://docs.digdir.no/docs/Maskinporten/maskinporten_guide_apikonsument

### Formatting

42 changes: 27 additions & 15 deletions Test/Altinn.Broker.Tests/FileControllerTests.cs
Original file line number Diff line number Diff line change
@@ -6,23 +6,35 @@
using Altinn.Broker.Core.Models;
using Altinn.Broker.Enums;
using Altinn.Broker.Models;
using Altinn.Broker.Tests.Helpers;

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Identity.Web;

using Xunit;

namespace Altinn.Broker.Tests;
public class FileControllerTests : IClassFixture<WebApplicationFactory<Program>>
public class FileControllerTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
private readonly HttpClient _senderClient;
private readonly HttpClient _recipientClient;
private readonly JsonSerializerOptions _responseSerializerOptions;

public FileControllerTests(WebApplicationFactory<Program> factory)
/**
* Inject a mock bearer configuration that does not verify anything.
* Generate our own JWT with correct scope, expiry and issuer.
* Set it as default request header
* */

public FileControllerTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTkyOjk5MTgyNTgyNyJ9.exFSD-mL1fzoWhg8IKcVeCeEyJ5qpABPU9A1AXHDa_k");
_senderClient = factory.CreateClient();
_senderClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TestConstants.DUMMY_SENDER_TOKEN);
_recipientClient = factory.CreateClient();
_recipientClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TestConstants.DUMMY_RECIPIENT_TOKEN);

_responseSerializerOptions = new JsonSerializerOptions(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
@@ -34,45 +46,45 @@ public FileControllerTests(WebApplicationFactory<Program> factory)
[Fact]
public async Task WhenAllIsOk_NormalFlow_Success()
{
var initializeFileResponse = await _client.PostAsJsonAsync("broker/api/v1/file", new FileInitalizeExt()
var initializeFileResponse = await _senderClient.PostAsJsonAsync("broker/api/v1/file", new FileInitalizeExt()
{
Checksum = null,
FileName = "input.txt",
PropertyList = [],
Recipients = new List<string> { "974761076" },
Sender = "991825827",
Recipients = new List<string> { "0192:986252932" },
Sender = "0192:991825827",
SendersFileReference = "test-data"
});
var fileId = await initializeFileResponse.Content.ReadAsStringAsync();

var initializedFile = await _client.GetFromJsonAsync<FileOverviewExt>($"broker/api/v1/file/{fileId}", _responseSerializerOptions);
var initializedFile = await _senderClient.GetFromJsonAsync<FileOverviewExt>($"broker/api/v1/file/{fileId}", _responseSerializerOptions);
Assert.NotNull(initializedFile);
Assert.True(initializedFile.FileStatus == FileStatusExt.Initialized);

var uploadedFileBytes = Encoding.UTF8.GetBytes("This is the contents of the uploaded file");
using (var content = new ByteArrayContent(uploadedFileBytes))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var uploadResponse = await _client.PostAsync($"broker/api/v1/file/{fileId}/upload", content);
var uploadResponse = await _senderClient.PostAsync($"broker/api/v1/file/{fileId}/upload", content);
Assert.True(uploadResponse.IsSuccessStatusCode);
}

var uploadedFile = await _client.GetFromJsonAsync<FileOverviewExt>($"broker/api/v1/file/{fileId}", _responseSerializerOptions);
var uploadedFile = await _senderClient.GetFromJsonAsync<FileOverviewExt>($"broker/api/v1/file/{fileId}", _responseSerializerOptions);
Assert.NotNull(uploadedFile);
Assert.True(uploadedFile.FileStatus == FileStatusExt.Published); // When running integration test this happens instantly as of now.

var downloadedFile = await _client.GetAsync($"broker/api/v1/file/{fileId}/download");
var downloadedFile = await _recipientClient.GetAsync($"broker/api/v1/file/{fileId}/download");
var downloadedFileBytes = await downloadedFile.Content.ReadAsByteArrayAsync();
Assert.Equal(uploadedFileBytes, downloadedFileBytes);

var downloadedFileDetails = await _client.GetFromJsonAsync<FileStatusDetailsExt>($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions);
var downloadedFileDetails = await _senderClient.GetFromJsonAsync<FileStatusDetailsExt>($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions);
Assert.NotNull(downloadedFileDetails);
Assert.True(downloadedFileDetails.FileStatus == FileStatusExt.Published);
Assert.Contains(downloadedFileDetails.RecipientFileStatusHistory, recipient => recipient.RecipientFileStatusCode == RecipientFileStatusExt.DownloadStarted);

await _client.PostAsync($"broker/api/v1/file/{fileId}/confirmdownload", null);
await _recipientClient.PostAsync($"broker/api/v1/file/{fileId}/confirmdownload", null);

var confirmedFileDetails = await _client.GetFromJsonAsync<FileStatusDetailsExt>($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions);
var confirmedFileDetails = await _senderClient.GetFromJsonAsync<FileStatusDetailsExt>($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions);
Assert.NotNull(confirmedFileDetails);
Assert.True(confirmedFileDetails.FileStatus == FileStatusExt.AllConfirmedDownloaded);
Assert.Contains(confirmedFileDetails.RecipientFileStatusHistory, recipient => recipient.RecipientFileStatusCode == RecipientFileStatusExt.DownloadConfirmed);
44 changes: 44 additions & 0 deletions Test/Altinn.Broker.Tests/Helpers/CustomWebApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(
IWebHostBuilder builder)
{
// Overwrite registrations from Program.cs
builder.ConfigureTestServices((services) =>
{
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.Services.Configure<AuthenticationOptions>(o =>
{
o.SchemeMap.Clear();
((IList<AuthenticationSchemeBuilder>)o.Schemes).Clear();
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(async options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
RequireExpirationTime = false,
RequireSignedTokens = false,
SignatureValidator = delegate (string token, TokenValidationParameters parameters)
{
var jwt = new JsonWebToken(token);
return jwt;
}
};
});
});
}
}
7 changes: 7 additions & 0 deletions Test/Altinn.Broker.Tests/Helpers/TestConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Altinn.Broker.Tests.Helpers;
internal class TestConstants
{
public const string DUMMY_SENDER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6ImFsdGlubjpicm9rZXIud3JpdGUiLCJpc3MiOiJodHRwczovL3Rlc3QubWFza2lucG9ydGVuLm5vLyIsImNsaWVudF9hbXIiOiJwcml2YXRlX2tleV9qd3QiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzAxNjkzODkyLCJpYXQiOjE3MDE2OTM3NzIsImNsaWVudF9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsImp0aSI6IjNTcjJnVjl0N2pNOEVzOTRKUzNOeGhQaXk5Q2hMemd0aldIZEYtNDlhN0kiLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifSwic3VwcGxpZXIiOnsiYXV0aG9yaXR5IjoiaXNvNjUyMy1hY3RvcmlkLXVwaXMiLCJJRCI6IjAxOTI6OTkxODI1ODI3In19.o5dL4LsbL2bJNNFQEYqdgvJiEglX5GabXxm86QfcA14";

public const string DUMMY_RECIPIENT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6ImFsdGlubjpicm9rZXIucmVhZCIsImlzcyI6Imh0dHBzOi8vdGVzdC5tYXNraW5wb3J0ZW4ubm8vIiwiY2xpZW50X2FtciI6InByaXZhdGVfa2V5X2p3dCIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJleHAiOjE3MDE2OTM4OTIsImlhdCI6MTcwMTY5Mzc3MiwiY2xpZW50X2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwianRpIjoiM1NyMmdWOXQ3ak04RXM5NEpTM054aFBpeTlDaEx6Z3RqV0hkRi00OWE3SSIsImNvbnN1bWVyIjp7ImF1dGhvcml0eSI6ImlzbzY1MjMtYWN0b3JpZC11cGlzIiwiSUQiOiIwMTkyOjk4NjI1MjkzMiJ9LCJzdXBwbGllciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifX0.CeK5n5v2MF51RYoqtB5ezCXvXx35nrKiYuDKoje8vi4";
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ services:
volumes:
- ./Test/Altinn.Broker.Tests/Data:/flyway/sql
depends_on:
- database
- database_migration
entrypoint:
[
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Original file line number Diff line number Diff line change
@@ -12,15 +12,13 @@ public class AzureBrokerStorageService : IBrokerStorageService
private readonly IFileStore _fileStore;
private readonly IResourceManager _resourceManager;
private readonly IHostEnvironment _hostEnvironment;
private readonly AzureStorageOptions _azureStorageOptions;
private readonly ILogger<AzureBrokerStorageService> _logger;

public AzureBrokerStorageService(IFileStore fileStore, IResourceManager resourceManager, IHostEnvironment hostEnvironment, IOptions<AzureStorageOptions> options, ILogger<AzureBrokerStorageService> logger)
public AzureBrokerStorageService(IFileStore fileStore, IResourceManager resourceManager, IHostEnvironment hostEnvironment, ILogger<AzureBrokerStorageService> logger)
{
_fileStore = fileStore;
_resourceManager = resourceManager;
_hostEnvironment = hostEnvironment;
_azureStorageOptions = options.Value;
_logger = logger;
}

@@ -41,7 +39,7 @@ private async Task<string> GetConnectionString(ServiceOwnerEntity serviceOwnerEn
if (_hostEnvironment.IsDevelopment())
{
_logger.LogInformation("Running in development. Using local development storage.");
return _azureStorageOptions.ConnectionString;
return AzureConstants.AzuriteUrl;
}
return await _resourceManager.GetStorageConnectionString(serviceOwnerEntity);
}
Loading