Skip to content

Commit 2faacb3

Browse files
Test Bump
1 parent 5fa08d0 commit 2faacb3

File tree

89 files changed

+1736
-370
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1736
-370
lines changed

Modules/Intent.Modules.Blazor.Authentication/Templates/Templates/Client/AccessTokenResponse/AccessTokenResponseTemplatePartial.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Intent.Engine;
1+
using System;
2+
using System.Collections.Generic;
3+
using Intent.Engine;
24
using Intent.Modules.Blazor.Authentication.Settings;
35
using Intent.Modules.Blazor.Settings;
46
using Intent.Modules.Common;
@@ -7,8 +9,6 @@
79
using Intent.Modules.Common.Templates;
810
using Intent.RoslynWeaver.Attributes;
911
using Intent.Templates;
10-
using System;
11-
using System.Collections.Generic;
1212

1313
[assembly: DefaultIntentManaged(Mode.Fully)]
1414
[assembly: IntentTemplate("Intent.ModuleBuilder.CSharp.Templates.CSharpTemplatePartial", Version = "1.0")]
@@ -113,10 +113,11 @@ public AccessTokenResponseTemplate(IOutputTarget outputTarget, object model = nu
113113

114114
public override bool CanRunTemplate()
115115
{
116-
//2 Template need this
117-
//JWT Service
118-
//PersistentAuthenticationStateProviderTemplate
119-
return base.CanRunTemplate() && (!ExecutionContext.GetSettings().GetBlazor().RenderMode().IsInteractiveServer() || ExecutionContext.GetSettings().GetBlazor().Authentication().IsJwt());
116+
//3 Templates need this
117+
// JWT Auth Service
118+
// OICD Auth Service
119+
// PersistentAuthenticationStateProviderTemplate
120+
return base.CanRunTemplate() && (!ExecutionContext.GetSettings().GetBlazor().RenderMode().IsInteractiveServer() || ExecutionContext.GetSettings().GetBlazor().Authentication().IsJwt() || ExecutionContext.GetSettings().GetBlazor().Authentication().IsOidc());
120121
}
121122

122123
[IntentManaged(Mode.Fully)]

Modules/Intent.Modules.Blazor.Authentication/Templates/Templates/Client/PersistentAuthenticationStateProvider/PersistentAuthenticationStateProviderTemplatePartial.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public PersistentAuthenticationStateProviderTemplate(IOutputTarget outputTarget,
3030
.AddUsing("System.Threading.Tasks")
3131
.AddUsing("Microsoft.AspNetCore.Components.WebAssembly.Authentication")
3232
.AddUsing("System")
33+
.AddUsing("System.Net.Http.Json")
3334
.AddClass($"PersistentAuthenticationStateProvider", @class =>
3435
{
3536
@class.WithBaseType("AuthenticationStateProvider");
@@ -45,7 +46,8 @@ public PersistentAuthenticationStateProviderTemplate(IOutputTarget outputTarget,
4546
f.WithAssignment(new CSharpStatement("_defaultUnauthenticatedTask"));
4647
});
4748
@class.AddField("Uri?", "_identityUrl", p => p.PrivateReadOnly());
48-
@class.AddField("HttpClient", "_refreshClient", p => p.PrivateReadOnly().WithAssignment("new HttpClient()"));
49+
50+
@class.AddField(UseType("System.Net.Http.HttpClient"), "_refreshClient", p => p.PrivateReadOnly().WithAssignment("new HttpClient()"));
4951
@class.AddField("string?", "_accessToken", p => p.Private());
5052
@class.AddField("string?", "_refreshToken", p => p.Private());
5153
@class.AddField("DateTimeOffset", "_accessTokenExpiresAt", p => p.Private().WithAssignment("DateTimeOffset.MinValue"));

Modules/Intent.Modules.Blazor.Authentication/Templates/Templates/Server/OidcAuthServiceConcrete/OidcAuthServiceConcreteTemplatePartial.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public OidcAuthServiceConcreteTemplate(IOutputTarget outputTarget, object model
7676
throw new InvalidOperationException(""OIDC authentication options are not configured correctly."");
7777
}", s => s.SeparatedFromNext());
7878

79+
method.AddStatement("var httpContext = _httpContextAccessor.HttpContext ?? throw new InvalidOperationException(\"No active HttpContext found.\");");
80+
method.AddStatement("await httpContext.SignOutAsync();", s => s.SeparatedFromNext());
81+
82+
7983
method.AddStatements(@"var tokenRequest = new Dictionary<string, string>
8084
{
8185
{ ""grant_type"", ""password"" },
@@ -90,13 +94,16 @@ public OidcAuthServiceConcreteTemplate(IOutputTarget outputTarget, object model
9094

9195
method.AddIfStatement("tokenResponse.IsSuccessStatusCode", @if =>
9296
{
93-
@if.AddAssignmentStatement("var tokens", new CSharpStatement("await tokenResponse.Content.ReadFromJsonAsync<AccessTokenResponse>();"));
97+
@if.AddAssignmentStatement("var loginResponse", new CSharpStatement($"await tokenResponse.Content.ReadFromJsonAsync<{this.GetAccessTokenResponseTemplateName()}>();"));
9498
@if.AddStatements(@"var claims = new List<Claim>
9599
{
96100
new Claim(ClaimTypes.NameIdentifier, username),
97101
new Claim(ClaimTypes.Email, username),
98-
new Claim(""access_token"", tokens.AccessToken),
99-
new Claim(""refresh_token"", tokens.RefreshToken)
102+
new Claim(""access_token"", loginResponse.AccessToken),
103+
new Claim(""refresh_token"", loginResponse.RefreshToken),
104+
new Claim(""token_type"", loginResponse.TokenType ?? ""Bearer""),
105+
new Claim(""expires_at"", (loginResponse.ExpiresIn ?? DateTime.UtcNow.AddHours(1)).ToString(""o""))
106+
100107
};".ConvertToStatements());
101108
@if.AddAssignmentStatement("var claimsIdentity", new CSharpStatement("new ClaimsIdentity(claims);"));
102109
@if.AddAssignmentStatement("var claimsPrincipal", new CSharpStatement("new ClaimsPrincipal(claimsIdentity);"));
@@ -107,14 +114,6 @@ public OidcAuthServiceConcreteTemplate(IOutputTarget outputTarget, object model
107114
@else.AddStatement("throw new Exception(\"Error: Invalid login attempt.\");");
108115
});
109116
});
110-
111-
@class.AddNestedClass("AccessTokenResponse", nested =>
112-
{
113-
nested.AddProperty("string", "AccessToken");
114-
nested.AddProperty("string", "RefreshToken");
115-
nested.AddProperty("string", "TokenType");
116-
nested.AddProperty("DateTime", "ExpiresIn");
117-
});
118117
});
119118
}
120119

Modules/Intent.Modules.Blazor.Authentication/release-notes.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
### Version 1.0.5
1+
### Version 1.0.6
22

33
- Improvement: Fixed AntiForgery exception when re-logging in.
4+
- Improvement: Refresh token support for WASM / JWT authentication.
5+
6+
### Version 1.0.5
7+
48
- Improvement: Updated module documentation to use centralized documentation site.
59

610
### Version 1.0.4

Modules/Intent.Modules.Blazor/Intent.Blazor.imodspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<package>
33
<id>Intent.Blazor</id>
4-
<version>1.0.6-pre.1</version>
4+
<version>1.0.7-pre.0</version>
55
<supportedClientVersions>[4.5.0-a, 5.0.0-a)</supportedClientVersions>
66
<summary>Automates the bootstrapping and core patterns of a Blazor web application.</summary>
77
<description>Automates the bootstrapping and core patterns of a Blazor web application.</description>

Modules/Intent.Modules.Blazor/release-notes.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
### Version 1.0.7
2+
3+
- Improvement: Improved WASM configuration for debugging.
4+
- Improvement: Help Topic around routing parameters..
5+
16
### Version 1.0.6
27

38
- Improvement: Updated module documentation to use centralized documentation site.
4-
- Improvement: Improved WASM configuration for debugging.
59

610
### Version 1.0.5
711

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Net.Http.Json;
12
using System.Security.Claims;
3+
using Blazor.InteractiveAuto.AspNetCoreIdentity.Client.Components.Account;
24
using Blazor.InteractiveAuto.AspNetCoreIdentity.Client.Components.Account.Shared;
35
using Intent.RoslynWeaver.Attributes;
46
using Microsoft.AspNetCore.Components;
@@ -12,47 +14,137 @@ namespace Blazor.InteractiveAuto.AspNetCoreIdentity.Client.Common
1214
{
1315
public class PersistentAuthenticationStateProvider : AuthenticationStateProvider, IAccessTokenProvider
1416
{
15-
private static readonly Task<AuthenticationState> defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
16-
private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask;
17+
private static readonly Task<AuthenticationState> _defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
18+
private readonly Task<AuthenticationState> _authenticationStateTask = _defaultUnauthenticatedTask;
19+
private readonly Uri? _identityUrl;
20+
private readonly HttpClient _refreshClient = new HttpClient();
21+
private readonly NavigationManager _nav;
22+
private string? _accessToken;
23+
private string? _refreshToken;
24+
private DateTimeOffset _accessTokenExpiresAt = DateTimeOffset.MinValue;
1725

18-
public PersistentAuthenticationStateProvider(PersistentComponentState state)
26+
public PersistentAuthenticationStateProvider(PersistentComponentState state, NavigationManager nav)
1927
{
28+
_nav = nav;
2029
if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
2130
{
2231
return;
2332
}
2433
Claim[] claims = [
2534
new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
2635
new Claim(ClaimTypes.Email, userInfo.Email),
27-
new Claim(ClaimTypes.Email, userInfo.Email),
2836
new Claim("access_token", userInfo.AccessToken == null ? "" : userInfo.AccessToken) ];
29-
authenticationStateTask = Task.FromResult(
37+
_authenticationStateTask = Task.FromResult(
3038
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
3139
authenticationType: nameof(PersistentAuthenticationStateProvider)))));
40+
41+
if (!string.IsNullOrWhiteSpace(userInfo.AccessToken))
42+
{
43+
_accessToken = userInfo.AccessToken;
44+
_refreshToken = userInfo.RefreshToken;
45+
46+
if (userInfo.AccessTokenExpiresAt.HasValue)
47+
{
48+
_accessTokenExpiresAt = userInfo.AccessTokenExpiresAt.Value;
49+
}
50+
51+
if (!string.IsNullOrEmpty(userInfo.RefreshUrl))
52+
{
53+
_identityUrl = new Uri(userInfo.RefreshUrl, UriKind.Absolute);
54+
}
55+
}
3256
}
3357

3458
public override Task<AuthenticationState> GetAuthenticationStateAsync()
3559
{
36-
return authenticationStateTask;
60+
return _authenticationStateTask;
3761
}
3862

39-
public async ValueTask<AccessTokenResult> RequestAccessToken()
63+
public ValueTask<AccessTokenResult> RequestAccessToken()
64+
=> RequestAccessToken(new AccessTokenRequestOptions());
65+
66+
public async ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
4067
{
41-
var state = await this.GetAuthenticationStateAsync();
42-
var token = state.User.FindFirst("access_token");
68+
var missingToken = string.IsNullOrWhiteSpace(_accessToken);
69+
var expired = _accessTokenExpiresAt > DateTimeOffset.MinValue && _accessTokenExpiresAt <= DateTimeOffset.UtcNow;
4370

44-
if (token == null)
71+
if (missingToken || expired)
4572
{
46-
return new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect, null, "auth/login", null);
73+
// Try to refresh if we have a refresh token
74+
if (!string.IsNullOrWhiteSpace(_refreshToken))
75+
{
76+
var refreshed = await TryRefreshAccessTokenAsync();
77+
if (refreshed)
78+
{
79+
// we now have a new _accessToken / _accessTokenExpiresAt
80+
var at = new AccessToken
81+
{
82+
Value = _accessToken!,
83+
Expires = _accessTokenExpiresAt
84+
};
85+
86+
return new AccessTokenResult(AccessTokenResultStatus.Success, at, null, null);
87+
}
88+
}
89+
90+
// No refresh token OR refresh failed → send user to login
91+
var current = _nav.ToBaseRelativePath(_nav.Uri);
92+
var returnUrl = "/" + current;
93+
var loginUrl = $"/account/login?returnUrl={Uri.EscapeDataString(returnUrl)}";
94+
95+
_nav.NavigateTo(loginUrl, forceLoad: true);
96+
97+
return new AccessTokenResult(
98+
AccessTokenResultStatus.RequiresRedirect, null, loginUrl, null);
4799
}
48-
var accessToken = new AccessToken { Expires = DateTimeOffset.MaxValue, Value = token.Value };
49-
var result = new AccessTokenResult(AccessTokenResultStatus.Success, accessToken, null, null);
50-
return result;
100+
101+
// Token present and we consider it valid
102+
var expires = _accessTokenExpiresAt > DateTimeOffset.MinValue
103+
? _accessTokenExpiresAt
104+
: DateTimeOffset.UtcNow.AddMinutes(5);
105+
106+
var accessToken = new AccessToken
107+
{
108+
Value = _accessToken!,
109+
Expires = expires
110+
};
111+
return new AccessTokenResult(AccessTokenResultStatus.Success, accessToken, null, null);
51112
}
52113

53-
public async ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
114+
private async Task<bool> TryRefreshAccessTokenAsync()
54115
{
55-
return await RequestAccessToken();
116+
if (string.IsNullOrWhiteSpace(_refreshToken) || _identityUrl == null)
117+
return false;
118+
119+
try
120+
{
121+
var refreshUri = new Uri(_identityUrl, "refresh"); // e.g. https://ids.example.com/refresh
122+
123+
var response = await _refreshClient.PostAsJsonAsync(refreshUri, new
124+
{
125+
refreshToken = _refreshToken
126+
});
127+
128+
if (!response.IsSuccessStatusCode)
129+
return false;
130+
131+
var dto = await response.Content.ReadFromJsonAsync<AccessTokenResponse>();
132+
if (dto is null || string.IsNullOrWhiteSpace(dto.AccessToken))
133+
return false;
134+
135+
_accessToken = dto.AccessToken;
136+
_refreshToken = string.IsNullOrWhiteSpace(dto.RefreshToken)
137+
? _refreshToken // keep old if not rotated
138+
: dto.RefreshToken;
139+
140+
// compute expiry
141+
_accessTokenExpiresAt = new DateTimeOffset(dto.ExpiresIn!.Value, TimeSpan.Zero);
142+
return true;
143+
}
144+
catch
145+
{
146+
return false;
147+
}
56148
}
57149
}
58150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Intent.RoslynWeaver.Attributes;
4+
5+
[assembly: DefaultIntentManaged(Mode.Fully)]
6+
[assembly: IntentTemplate("Intent.Blazor.Authentication.Templates.Client.AccessTokenResponseTemplate", Version = "1.0")]
7+
8+
namespace Blazor.InteractiveAuto.AspNetCoreIdentity.Client.Components.Account
9+
{
10+
public class AccessTokenResponse
11+
{
12+
public string AccessToken { get; set; }
13+
public string RefreshToken { get; set; }
14+
public string? TokenType { get; set; }
15+
[JsonConverter(typeof(NullableExpiresInConverter))]
16+
public DateTime? ExpiresIn { get; set; }
17+
public class NullableExpiresInConverter : JsonConverter<DateTime?>
18+
{
19+
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
20+
{
21+
// JSON null → null
22+
if (reader.TokenType == JsonTokenType.Null)
23+
return null;
24+
25+
// Number → seconds
26+
if (reader.TokenType == JsonTokenType.Number)
27+
{
28+
if (reader.TryGetInt64(out var seconds))
29+
return DateTime.UtcNow.AddSeconds(seconds);
30+
31+
throw new JsonException("expiresIn number is not Int64.");
32+
}
33+
34+
// String (ISO date, seconds, empty, or null-like)
35+
if (reader.TokenType == JsonTokenType.String)
36+
{
37+
var raw = reader.GetString();
38+
39+
// "" or "null" → null
40+
if (string.IsNullOrWhiteSpace(raw) || raw.Equals("null", StringComparison.OrdinalIgnoreCase))
41+
return null;
42+
43+
// ISO timestamp
44+
if (DateTimeOffset.TryParse(raw, out var dto))
45+
return dto.UtcDateTime;
46+
47+
// seconds as string
48+
if (long.TryParse(raw, out var seconds))
49+
return DateTime.UtcNow.AddSeconds(seconds);
50+
51+
throw new JsonException($"Cannot parse expiresIn value: {raw}");
52+
}
53+
54+
throw new JsonException(
55+
$"Unexpected token type for expiresIn: {reader.TokenType}");
56+
}
57+
58+
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
59+
{
60+
if (value == null)
61+
{
62+
writer.WriteNullValue();
63+
return;
64+
}
65+
66+
var utc = value.Value.Kind == DateTimeKind.Utc
67+
? value.Value
68+
: value.Value.ToUniversalTime();
69+
70+
long seconds = (long)Math.Max(
71+
0,
72+
(utc - DateTime.UtcNow).TotalSeconds);
73+
74+
writer.WriteNumberValue(seconds);
75+
}
76+
}
77+
}
78+
}

Tests/Blazor.InteractiveAuto.AspNetCoreIdentity/Blazor.InteractiveAuto.AspNetCoreIdentity.Client/Components/Account/Shared/UserInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ public class UserInfo
1010
public required string UserId { get; set; }
1111
public required string Email { get; set; }
1212
public string? AccessToken { get; set; }
13+
public string? RefreshToken { get; set; }
14+
public string? RefreshUrl { get; set; }
15+
public DateTime? AccessTokenExpiresAt { get; set; }
1316
}
1417
}

0 commit comments

Comments
 (0)