diff --git a/Digdir.Domain.Dialogporten.sln b/Digdir.Domain.Dialogporten.sln
index f513035db..9d6ebfded 100644
--- a/Digdir.Domain.Dialogporten.sln
+++ b/Digdir.Domain.Dialogporten.sln
@@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Tool.Dialogporten.Ed
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Domain.Dialogporten.WebApi.Integration.Tests", "tests\Digdir.Domain.Dialogporten.WebApi.Integration.Tests\Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj", "{42004236-D45C-4A1F-9FF9-CF12B7388389}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Domain.Dialogporten.GraphQL", "src\Digdir.Domain.Dialogporten.GraphQL\Digdir.Domain.Dialogporten.GraphQL.csproj", "{234FE24D-1047-4E29-A625-1EB406C37A2D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -123,6 +125,10 @@ Global
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Release|Any CPU.Build.0 = Release|Any CPU
+ {234FE24D-1047-4E29-A625-1EB406C37A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {234FE24D-1047-4E29-A625-1EB406C37A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {234FE24D-1047-4E29-A625-1EB406C37A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {234FE24D-1047-4E29-A625-1EB406C37A2D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -147,6 +153,7 @@ Global
{B6FE45A3-FB14-4528-9957-295AB2A00A46} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7}
{030909AA-5B61-46B4-9B74-0D2D779478FF} = {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633}
{42004236-D45C-4A1F-9FF9-CF12B7388389} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7}
+ {234FE24D-1047-4E29-A625-1EB406C37A2D} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B2FE67FF-7622-4AFB-AD8E-961B6A39D888}
diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogStatus.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogStatus.cs
index 7443da2af..a1d2c0bdd 100644
--- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogStatus.cs
+++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogStatus.cs
@@ -10,13 +10,13 @@ public DialogStatus(Values id) : base(id) { }
public enum Values
{
///
- /// Dialogen er å regne som ny. Brukes typisk for enkle meldinger som ikke krever noe
- /// interaksjon, eller som et initielt steg for dialoger. Dette er default.
+ /// Dialogen er å regne som ny. Brukes typisk for enkle meldinger som ikke krever noe
+ /// interaksjon, eller som et initielt steg for dialoger. Dette er default.
///
New = 1,
///
- /// Under arbeid. Generell status som brukes for dialogtjenester der ytterligere bruker-input er
+ /// Under arbeid. Generell status som brukes for dialogtjenester der ytterligere bruker-input er
/// forventet.
///
InProgress = 2,
@@ -27,8 +27,8 @@ public enum Values
Waiting = 3,
///
- /// Dialogen er i en tilstand hvor den venter på signering. Typisk siste steg etter at all
- /// utfylling er gjennomført og validert.
+ /// Dialogen er i en tilstand hvor den venter på signering. Typisk siste steg etter at all
+ /// utfylling er gjennomført og validert.
///
Signing = 4,
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationBuilderExtensions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationBuilderExtensions.cs
new file mode 100644
index 000000000..5ddbc6c0f
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationBuilderExtensions.cs
@@ -0,0 +1,63 @@
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using System.Diagnostics;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+internal static class AuthenticationBuilderExtensions
+{
+ public static IServiceCollection AddDialogportenAuthentication(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ var jwtTokenSchemas = configuration
+ .GetSection(GraphQlSettings.SectionName)
+ .Get()
+ ?.Authentication
+ ?.JwtBearerTokenSchemas;
+
+ if (jwtTokenSchemas is null || jwtTokenSchemas.Count == 0)
+ // Validation should have caught this.
+ throw new UnreachableException();
+
+ services.AddSingleton();
+
+ var authenticationBuilder = services.AddAuthentication();
+
+ foreach (var schema in jwtTokenSchemas)
+ {
+ authenticationBuilder.AddJwtBearer(schema.Name, options =>
+ {
+ options.MetadataAddress = schema.WellKnown;
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ ValidateIssuer = false,
+ ValidateAudience = false,
+ RequireExpirationTime = true,
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.FromSeconds(2)
+ };
+
+ options.Events = new JwtBearerEvents
+ {
+ OnMessageReceived = async context =>
+ {
+ var expectedIssuer = await context.HttpContext
+ .RequestServices
+ .GetRequiredService()
+ .GetIssuerForScheme(schema.Name);
+
+ if (context.HttpContext.Items.TryGetValue(Constants.CurrentTokenIssuer, out var tokenIssuer)
+ && (string?)tokenIssuer != expectedIssuer)
+ {
+ context.NoResult();
+ }
+ }
+ };
+ });
+ }
+
+ return services;
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptions.cs
new file mode 100644
index 000000000..7c17dd258
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptions.cs
@@ -0,0 +1,12 @@
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+public sealed class AuthenticationOptions
+{
+ public required List JwtBearerTokenSchemas { get; init; }
+}
+
+public sealed class JwtBearerTokenSchemasOptions
+{
+ public required string Name { get; init; }
+ public required string WellKnown { get; init; }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptionsValidator.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptionsValidator.cs
new file mode 100644
index 000000000..cb3b95620
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptionsValidator.cs
@@ -0,0 +1,25 @@
+using FluentValidation;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+internal sealed class AuthenticationOptionsValidator : AbstractValidator
+{
+ public AuthenticationOptionsValidator(
+ IValidator jwtTokenSchemaValidator)
+ {
+ RuleFor(x => x.JwtBearerTokenSchemas)
+ .NotEmpty()
+ .WithMessage("At least one JwtBearerTokenSchema must be configured");
+ RuleForEach(x => x.JwtBearerTokenSchemas)
+ .SetValidator(jwtTokenSchemaValidator);
+ }
+}
+
+internal sealed class JwtBearerTokenSchemasOptionsValidator : AbstractValidator
+{
+ public JwtBearerTokenSchemasOptionsValidator()
+ {
+ RuleFor(x => x.Name).NotEmpty();
+ RuleFor(x => x.WellKnown).NotEmpty();
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/JwtSchemeSelectorMiddleware.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/JwtSchemeSelectorMiddleware.cs
new file mode 100644
index 000000000..4c73edb27
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/JwtSchemeSelectorMiddleware.cs
@@ -0,0 +1,40 @@
+using System.IdentityModel.Tokens.Jwt;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+public class JwtSchemeSelectorMiddleware
+{
+ private readonly RequestDelegate _next;
+
+ public JwtSchemeSelectorMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public Task InvokeAsync(HttpContext context)
+ {
+ if (!context.Request.Headers.TryGetValue(Constants.Authorization, out var authorizationHeader))
+ return _next(context);
+
+ var token = authorizationHeader.ToString()
+ .Split(' ')
+ .LastOrDefault();
+
+ if (string.IsNullOrEmpty(token))
+ return _next(context);
+
+ var handler = new JwtSecurityTokenHandler();
+ if (!handler.CanReadToken(token))
+ return _next(context);
+
+ var jwtToken = handler.ReadJwtToken(token);
+ context.Items[Constants.CurrentTokenIssuer] = jwtToken.Issuer;
+ return _next(context);
+ }
+}
+
+public static class JwtSchemeSelectorMiddlewareExtensions
+{
+ public static IApplicationBuilder UseJwtSchemeSelector(this IApplicationBuilder app)
+ => app.UseMiddleware();
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/LocalDevelopmentUser.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/LocalDevelopmentUser.cs
new file mode 100644
index 000000000..499f1b16f
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/LocalDevelopmentUser.cs
@@ -0,0 +1,44 @@
+using System.Collections.ObjectModel;
+using System.Security.Claims;
+using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+internal sealed class LocalDevelopmentUser : IUser
+{
+ private readonly ClaimsPrincipal _principal = new(new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.Name, "Local Development User"),
+ new Claim(ClaimTypes.NameIdentifier, "local-development-user"),
+ new Claim("pid", "03886595947"),
+ new Claim("scope", string.Join(" ", AuthorizationScope.AllScopes.Value)),
+ new Claim("consumer",
+ """
+ {
+ "authority": "iso6523-actorid-upis",
+ "ID": "0192:991825827"
+ }
+ """)
+ }));
+
+ public ClaimsPrincipal GetPrincipal() => _principal;
+}
+
+
+internal static class AuthorizationScope
+{
+ public const string EndUser = "digdir:dialogporten";
+ public const string ServiceProvider = "digdir:dialogporten.serviceprovider";
+ public const string ServiceProviderSearch = "digdir:dialogporten.serviceprovider.search";
+ public const string Testing = "digdir:dialogporten.developer.test";
+
+ internal static readonly Lazy> AllScopes = new(GetAll);
+
+ private static ReadOnlyCollection GetAll() =>
+ typeof(AuthorizationScope)
+ .GetFields()
+ .Where(x => x.IsLiteral && !x.IsInitOnly && x.DeclaringType == typeof(string))
+ .Select(x => (string)x.GetRawConstantValue()!)
+ .ToList()
+ .AsReadOnly();
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/TokenIssuerCache.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/TokenIssuerCache.cs
new file mode 100644
index 000000000..f2ffe53c5
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/TokenIssuerCache.cs
@@ -0,0 +1,64 @@
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.Extensions.Options;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+
+public interface ITokenIssuerCache
+{
+ public Task GetIssuerForScheme(string schemeName);
+}
+
+public sealed class TokenIssuerCache : ITokenIssuerCache, IDisposable
+{
+ private readonly Dictionary _issuerMappings = new();
+ private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
+ private bool _initialized;
+ private readonly IReadOnlyCollection _jwtTokenSchemas;
+
+ public TokenIssuerCache(IOptions apiSettings)
+ {
+ _jwtTokenSchemas = apiSettings
+ .Value
+ .Authentication
+ .JwtBearerTokenSchemas
+ ?? throw new ArgumentException("JwtBearerTokenSchemas is required.");
+ }
+
+ public async Task GetIssuerForScheme(string schemeName)
+ {
+ await EnsureInitializedAsync();
+
+ return _issuerMappings.TryGetValue(schemeName, out var issuer)
+ ? issuer : null;
+ }
+
+ private async Task EnsureInitializedAsync()
+ {
+ if (_initialized) return;
+ await _initializationSemaphore.WaitAsync();
+ if (_initialized) return;
+
+ try
+ {
+ foreach (var schema in _jwtTokenSchemas)
+ {
+ var configManager = new ConfigurationManager(
+ schema.WellKnown, new OpenIdConnectConfigurationRetriever());
+ var config = await configManager.GetConfigurationAsync();
+ _issuerMappings[schema.Name] = config.Issuer;
+ }
+
+ _initialized = true;
+ }
+ finally
+ {
+ _initializationSemaphore.Release();
+ }
+ }
+
+ public void Dispose()
+ {
+ _initializationSemaphore.Dispose();
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AllowAnonymousHandler.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AllowAnonymousHandler.cs
new file mode 100644
index 000000000..59478a730
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AllowAnonymousHandler.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
+
+///
+/// This authorisation handler will bypass all requirements
+///
+public class AllowAnonymousHandler : IAuthorizationHandler
+{
+ public Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ foreach (var requirement in context.PendingRequirements)
+ {
+ //Simply pass all requirements
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationOptionsSetup.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationOptionsSetup.cs
new file mode 100644
index 000000000..3af13c46e
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationOptionsSetup.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Extensions.Options;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
+
+internal sealed class AuthorizationOptionsSetup : IConfigureOptions
+{
+ private readonly GraphQlSettings _options;
+
+ public AuthorizationOptionsSetup(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ public void Configure(AuthorizationOptions options)
+ {
+ var authenticationSchemas = _options
+ .Authentication
+ .JwtBearerTokenSchemas
+ .Select(x => x.Name)
+ .ToArray();
+
+ options.DefaultPolicy = new AuthorizationPolicyBuilder()
+ .RequireAuthenticatedUser()
+ .AddAuthenticationSchemes(authenticationSchemas)
+ .RequireValidConsumerClaim()
+ .Build();
+
+ options.AddPolicy(AuthorizationPolicy.EndUser, builder => builder
+ .Combine(options.DefaultPolicy)
+ .RequireScope(AuthorizationScope.EndUser));
+
+ options.AddPolicy(AuthorizationPolicy.ServiceProvider, builder => builder
+ .Combine(options.DefaultPolicy)
+ .RequireScope(AuthorizationScope.ServiceProvider));
+
+ options.AddPolicy(AuthorizationPolicy.ServiceProviderSearch, builder => builder
+ .Combine(options.DefaultPolicy)
+ .RequireScope(AuthorizationScope.ServiceProviderSearch));
+
+ options.AddPolicy(AuthorizationPolicy.Testing, builder => builder
+ .Combine(options.DefaultPolicy)
+ .RequireScope(AuthorizationScope.Testing));
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicy.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicy.cs
new file mode 100644
index 000000000..9a227445a
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicy.cs
@@ -0,0 +1,29 @@
+using System.Collections.ObjectModel;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
+
+internal static class AuthorizationPolicy
+{
+ public const string EndUser = "enduser";
+ public const string ServiceProvider = "serviceprovider";
+ public const string ServiceProviderSearch = "serviceproviderSearch";
+ public const string Testing = "testing";
+}
+
+internal static class AuthorizationScope
+{
+ public const string EndUser = "digdir:dialogporten";
+ public const string ServiceProvider = "digdir:dialogporten.serviceprovider";
+ public const string ServiceProviderSearch = "digdir:dialogporten.serviceprovider.search";
+ public const string Testing = "digdir:dialogporten.developer.test";
+
+ internal static readonly Lazy> AllScopes = new(GetAll);
+
+ private static ReadOnlyCollection GetAll() =>
+ typeof(AuthorizationScope)
+ .GetFields()
+ .Where(x => x.IsLiteral && !x.IsInitOnly && x.DeclaringType == typeof(string))
+ .Select(x => (string)x.GetRawConstantValue()!)
+ .ToList()
+ .AsReadOnly();
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicyBuilderExtensions.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicyBuilderExtensions.cs
new file mode 100644
index 000000000..d1d2e2395
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationPolicyBuilderExtensions.cs
@@ -0,0 +1,21 @@
+using Digdir.Domain.Dialogporten.Application.Common.Extensions;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
+
+internal static class AuthorizationPolicyBuilderExtensions
+{
+ private const string ScopeClaim = "scope";
+ private const char ScopeClaimSeparator = ' ';
+
+ public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, string scope) =>
+ builder.RequireAssertion(ctx => ctx.User.Claims
+ .Where(x => x.Type == ScopeClaim)
+ .Select(x => x.Value)
+ .Any(scopeValue => scopeValue == scope || scopeValue
+ .Split(ScopeClaimSeparator, StringSplitOptions.RemoveEmptyEntries)
+ .Contains(scope)));
+
+ public static AuthorizationPolicyBuilder RequireValidConsumerClaim(this AuthorizationPolicyBuilder builder) =>
+ builder.RequireAssertion(ctx => ctx.User.TryGetOrgNumber(out _));
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/Constants.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Constants.cs
new file mode 100644
index 000000000..7d054cc36
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/Constants.cs
@@ -0,0 +1,8 @@
+namespace Digdir.Domain.Dialogporten.GraphQL.Common;
+
+internal static class Constants
+{
+ internal const string Authorization = "Authorization";
+ internal const string CurrentTokenIssuer = "CurrentIssuer";
+}
+
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Common/GraphQlSettings.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Common/GraphQlSettings.cs
new file mode 100644
index 000000000..e65004c58
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Common/GraphQlSettings.cs
@@ -0,0 +1,21 @@
+using Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;
+using FluentValidation;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.Common;
+
+public sealed class GraphQlSettings
+{
+ public const string SectionName = "GraphQl";
+
+ public required AuthenticationOptions Authentication { get; init; }
+}
+
+internal sealed class GraphQlOptionsValidator : AbstractValidator
+{
+ public GraphQlOptionsValidator(
+ IValidator authenticationOptionsValidator)
+ {
+ RuleFor(x => x.Authentication)
+ .SetValidator(authenticationOptionsValidator);
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj
new file mode 100644
index 000000000..b22356814
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ 750256a4-f332-4783-8802-8a7d9566f9ca
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/MappingProfile.cs
new file mode 100644
index 000000000..8924c36d8
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/MappingProfile.cs
@@ -0,0 +1,27 @@
+using AutoMapper;
+using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations;
+using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get;
+using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.Common;
+
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap()
+ .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type));
+ CreateMap()
+ .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type));
+
+ CreateMap()
+ .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type));
+ CreateMap()
+ .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type));
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/ObjectTypes.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/ObjectTypes.cs
new file mode 100644
index 000000000..f97895552
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/Common/ObjectTypes.cs
@@ -0,0 +1,90 @@
+namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.Common;
+
+public sealed class Localization
+{
+ public string Value { get; set; } = null!;
+ public string CultureCode { get; set; } = null!;
+}
+
+public enum ContentType
+{
+ Title = 1,
+ SenderName = 2,
+ Summary = 3,
+ AdditionalInfo = 4
+}
+
+public sealed class Content
+{
+ public ContentType Type { get; set; }
+ public List Value { get; set; } = [];
+}
+
+public sealed class SeenLog
+{
+ public Guid Id { get; set; }
+ public DateTimeOffset SeenAt { get; set; }
+
+ public string EndUserIdHash { get; set; } = null!;
+
+ public string? EndUserName { get; set; }
+
+ public bool IsCurrentEndUser { get; set; }
+}
+
+public sealed class Activity
+{
+ public Guid Id { get; set; }
+ public DateTimeOffset? CreatedAt { get; set; }
+ public Uri? ExtendedType { get; set; }
+
+ public ActivityType Type { get; set; }
+
+ public Guid? RelatedActivityId { get; set; }
+ public Guid? DialogElementId { get; set; }
+
+ public List? PerformedBy { get; set; } = [];
+ public List Description { get; set; } = [];
+}
+
+public enum ActivityType
+{
+ [GraphQLDescription("Refers to a submission made by a party that has been received by the service provider.")]
+ Submission = 1,
+
+ [GraphQLDescription("Indicates feedback from the service provider on a submission. Contains a reference to the current submission.")]
+ Feedback = 2,
+
+ [GraphQLDescription("Information from the service provider, not (directly) related to any submission.")]
+ Information = 3,
+
+ [GraphQLDescription("Used to indicate an error situation, typically on a submission. Contains a service-specific activityErrorCode.")]
+ Error = 4,
+
+ [GraphQLDescription("Indicates that the dialog is closed for further changes. This typically happens when the dialog is completed or deleted.")]
+ Closed = 5,
+
+ [GraphQLDescription("When the dialog is forwarded (delegated access) by someone with access to others.")]
+ Forwarded = 7
+}
+
+public enum DialogStatus
+{
+ [GraphQLDescription("New")]
+ New = 1,
+
+ [GraphQLDescription("In progress. General status used for dialog services where further user input is expected.")]
+ InProgress = 2,
+
+ [GraphQLDescription("Waiting for feedback from the service provider")]
+ Waiting = 3,
+
+ [GraphQLDescription("The dialog is in a state where it is waiting for signing. Typically the last step after all completion is carried out and validated.")]
+ Signing = 4,
+
+ [GraphQLDescription("The dialog was cancelled. This typically removes the dialog from normal GUI views.")]
+ Cancelled = 5,
+
+ [GraphQLDescription("The dialog was completed. This typically moves the dialog to a GUI archive or similar.")]
+ Completed = 6
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/MappingProfile.cs
new file mode 100644
index 000000000..c0c5c8ec3
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/MappingProfile.cs
@@ -0,0 +1,28 @@
+using AutoMapper;
+using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations;
+using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get;
+using Digdir.Domain.Dialogporten.GraphQL.EndUser.Common;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;
+
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status));
+
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.ConsumerType, opt => opt.MapFrom(src => src.ConsumerType));
+
+ CreateMap()
+ .ForMember(dest => dest.Priority, opt => opt.MapFrom(src => src.Priority));
+
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.HttpMethod, opt => opt.MapFrom(src => src.HttpMethod));
+
+ CreateMap();
+ }
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs
new file mode 100644
index 000000000..46a905d1f
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/ObjectTypes.cs
@@ -0,0 +1,121 @@
+using Digdir.Domain.Dialogporten.GraphQL.EndUser.Common;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;
+
+public sealed class Dialog
+{
+ public Guid Id { get; set; }
+ public Guid Revision { get; set; }
+ public string Org { get; set; } = null!;
+ public string ServiceResource { get; set; } = null!;
+ public string Party { get; set; } = null!;
+ public int? Progress { get; set; }
+ public string? ExtendedStatus { get; set; }
+ public string? ExternalReference { get; set; }
+ public DateTimeOffset? VisibleFrom { get; set; }
+ public DateTimeOffset? DueAt { get; set; }
+ public DateTimeOffset? ExpiresAt { get; set; }
+ public DateTimeOffset CreatedAt { get; set; }
+ public DateTimeOffset UpdatedAt { get; set; }
+
+ public string? DialogToken { get; set; }
+
+ public DialogStatus Status { get; set; }
+
+ public List Content { get; set; } = [];
+ public List Elements { get; set; } = [];
+ public List GuiActions { get; set; } = [];
+ public List ApiActions { get; set; } = [];
+ public List Activities { get; set; } = [];
+ public List SeenSinceLastUpdate { get; set; } = [];
+}
+
+public sealed class ApiAction
+{
+ public Guid Id { get; set; }
+ public string Action { get; set; } = null!;
+ public string? AuthorizationAttribute { get; set; }
+ public bool IsAuthorized { get; set; }
+
+ public Guid? DialogElementId { get; set; }
+
+ public List Endpoints { get; set; } = [];
+}
+
+// ReSharper disable InconsistentNaming
+public enum HttpVerb
+{
+ GET = 1,
+ POST = 2,
+ PUT = 3,
+ PATCH = 4,
+ DELETE = 5,
+ HEAD = 6,
+ OPTIONS = 7,
+ TRACE = 8,
+ CONNECT = 9
+}
+
+public sealed class ApiActionEndpoint
+{
+ public Guid Id { get; set; }
+ public string? Version { get; set; }
+ public Uri Url { get; set; } = null!;
+ public HttpVerb HttpMethod { get; set; }
+ public Uri? DocumentationUrl { get; set; }
+ public Uri? RequestSchema { get; set; }
+ public Uri? ResponseSchema { get; set; }
+ public bool Deprecated { get; set; }
+ public DateTimeOffset? SunsetAt { get; set; }
+}
+
+public sealed class GuiAction
+{
+ public Guid Id { get; set; }
+ public string Action { get; set; } = null!;
+ public Uri Url { get; set; } = null!;
+ public string? AuthorizationAttribute { get; set; }
+ public bool IsAuthorized { get; set; }
+ public bool IsBackChannel { get; set; }
+ public bool IsDeleteAction { get; set; }
+
+ public GuiActionPriority Priority { get; set; }
+
+ public List Title { get; set; } = [];
+}
+
+public enum GuiActionPriority
+{
+ Primary = 1,
+ Secondary = 2,
+ Tertiary = 3
+}
+
+public sealed class Element
+{
+ public Guid Id { get; set; }
+ public Uri? Type { get; set; }
+ public string? ExternalReference { get; set; }
+ public string? AuthorizationAttribute { get; set; }
+ public bool IsAuthorized { get; set; }
+
+ public Guid? RelatedDialogElementId { get; set; }
+
+ public List DisplayName { get; set; } = [];
+ public List Urls { get; set; } = [];
+}
+
+public sealed class ElementUrl
+{
+ public Guid Id { get; set; }
+ public Uri Url { get; set; } = null!;
+ public string? MimeType { get; set; }
+
+ public ElementUrlConsumer ConsumerType { get; set; }
+}
+
+public enum ElementUrlConsumer
+{
+ Gui = 1,
+ Api = 2
+}
diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs
new file mode 100644
index 000000000..bb26778c4
--- /dev/null
+++ b/src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogQueries.cs
@@ -0,0 +1,56 @@
+using AutoMapper;
+using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get;
+using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;
+using Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
+using Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;
+using Digdir.Domain.Dialogporten.GraphQL.EndUser.SearchDialogs;
+using HotChocolate.Authorization;
+using MediatR;
+
+namespace Digdir.Domain.Dialogporten.GraphQL.EndUser;
+
+[Authorize(Policy = AuthorizationPolicy.EndUser)]
+public class DialogQueries
+{
+ public async Task