Skip to content

Commit e0db08c

Browse files
authored
Use v2 API for EVEOnline provider (#497)
* Add EVEOnlineV2 provider * Rebased * Correct failing tests in EVEOnline provider * Add scopes back for EVEOnline provider * Add error handling where missing in EVEOnline provider * Alter exceptions from EVEOnline provider to InvalidOperationException * Remove redundant setting of GivenName claim for EVEOnline provider * Fixed broken rebase * Pull request edits for EVEOnline provider
1 parent f81ec3c commit e0db08c

File tree

9 files changed

+132
-42
lines changed

9 files changed

+132
-42
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ We would love it if you could help contributing to this repository.
7272
* [Luke Fulliton](https://github.com/lukefulliton)
7373
* [Mariusz Zieliński](https://github.com/mariozski)
7474
* [Martin Costello](https://github.com/martincostello)
75+
* [Matthew Moore](https://github.com/Dusty-Meg)
7576
* [Maxime Roussin-Bélanger](https://github.com/Lorac)
7677
* [Michael Knowles](https://github.com/mjknowles)
7778
* [Michael Tanczos](https://github.com/tanczosm)

src/AspNet.Security.OAuth.EVEOnline/AspNet.Security.OAuth.EVEOnline.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
<PropertyGroup>
88
<Description>ASP.NET Core security middleware enabling EVEOnline authentication.</Description>
9-
<Authors>Mariusz Zieliński;Chino Chang</Authors>
9+
<Authors>Mariusz Zieliński;Chino Chang;Matthew Moore</Authors>
1010
<PackageTags>aspnetcore;authentication;eveonline;oauth;security</PackageTags>
1111
</PropertyGroup>
1212

1313
<ItemGroup>
1414
<FrameworkReference Include="Microsoft.AspNetCore.App" />
1515
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
16+
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
1617
</ItemGroup>
1718

18-
</Project>
19+
</Project>

src/AspNet.Security.OAuth.EVEOnline/EVEOnlineAuthenticationDefaults.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ public static class Tranquility
3939
/// <summary>
4040
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
4141
/// </summary>
42-
public static readonly string AuthorizationEndpoint = "https://login.eveonline.com/oauth/authorize";
42+
public static readonly string AuthorizationEndpoint = "https://login.eveonline.com/v2/oauth/authorize";
4343

4444
/// <summary>
4545
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
4646
/// </summary>
47-
public static readonly string TokenEndpoint = "https://login.eveonline.com/oauth/token";
47+
public static readonly string TokenEndpoint = "https://login.eveonline.com/v2/oauth/token";
4848

4949
/// <summary>
5050
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
5151
/// </summary>
52+
[Obsolete("This endpoint is no longer used by the EVEOnline provider.")]
5253
public static readonly string UserInformationEndpoint = "https://login.eveonline.com/oauth/verify";
5354
}
5455

@@ -60,16 +61,17 @@ public static class Singularity
6061
/// <summary>
6162
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
6263
/// </summary>
63-
public static readonly string AuthorizationEndpoint = "https://sisilogin.testeveonline.com/oauth/authorize";
64+
public static readonly string AuthorizationEndpoint = "https://sisilogin.testeveonline.com/v2/oauth/authorize";
6465

6566
/// <summary>
6667
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
6768
/// </summary>
68-
public static readonly string TokenEndpoint = "https://sisilogin.testeveonline.com/oauth/token";
69+
public static readonly string TokenEndpoint = "https://sisilogin.testeveonline.com/v2/oauth/token";
6970

7071
/// <summary>
7172
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
7273
/// </summary>
74+
[Obsolete("This endpoint is no longer used by the EVEOnline provider.")]
7375
public static readonly string UserInformationEndpoint = "https://sisilogin.testeveonline.com/oauth/verify";
7476
}
7577
}

src/AspNet.Security.OAuth.EVEOnline/EVEOnlineAuthenticationExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66

77
using AspNet.Security.OAuth.EVEOnline;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
9+
using Microsoft.Extensions.Options;
810

911
namespace Microsoft.Extensions.DependencyInjection;
1012

@@ -69,6 +71,8 @@ public static AuthenticationBuilder AddEVEOnline(
6971
[CanBeNull] string caption,
7072
[NotNull] Action<EVEOnlineAuthenticationOptions> configuration)
7173
{
74+
builder.Services.TryAddSingleton<IPostConfigureOptions<EVEOnlineAuthenticationOptions>, EVEOnlinePostConfigureOptions>();
75+
7276
return builder.AddOAuth<EVEOnlineAuthenticationOptions, EVEOnlineAuthenticationHandler>(scheme, caption, configuration);
7377
}
7478
}

src/AspNet.Security.OAuth.EVEOnline/EVEOnlineAuthenticationHandler.cs

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
* for more information concerning the license and the contributors participating to this project.
55
*/
66

7-
using System.Net.Http.Headers;
7+
using System.Globalization;
88
using System.Security.Claims;
99
using System.Text.Encodings.Web;
10-
using System.Text.Json;
1110
using Microsoft.Extensions.Logging;
1211
using Microsoft.Extensions.Options;
12+
using Microsoft.IdentityModel.JsonWebTokens;
1313

1414
namespace AspNet.Security.OAuth.EVEOnline;
1515

1616
public partial class EVEOnlineAuthenticationHandler : OAuthHandler<EVEOnlineAuthenticationOptions>
1717
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="EVEOnlineAuthenticationHandler"/> class.
20+
/// </summary>
21+
/// <param name="options">The authentication options.</param>
22+
/// <param name="logger">The logger to use.</param>
23+
/// <param name="encoder">The URL encoder to use.</param>
24+
/// <param name="clock">The system clock to use.</param>
1825
public EVEOnlineAuthenticationHandler(
1926
[NotNull] IOptionsMonitor<EVEOnlineAuthenticationOptions> options,
2027
[NotNull] ILoggerFactory logger,
@@ -29,27 +36,88 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
2936
[NotNull] AuthenticationProperties properties,
3037
[NotNull] OAuthTokenResponse tokens)
3138
{
32-
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
33-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
34-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
39+
string? accessToken = tokens.AccessToken;
3540

36-
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
37-
if (!response.IsSuccessStatusCode)
41+
if (string.IsNullOrWhiteSpace(accessToken))
3842
{
39-
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
40-
throw new HttpRequestException("An error occurred while retrieving the user profile.");
43+
throw new InvalidOperationException("No access token was returned in the OAuth token.");
4144
}
4245

43-
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
46+
var tokenClaims = ExtractClaimsFromToken(accessToken);
47+
48+
foreach (var claim in tokenClaims)
49+
{
50+
identity.AddClaim(claim);
51+
}
4452

4553
var principal = new ClaimsPrincipal(identity);
46-
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
54+
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, tokens.Response!.RootElement);
4755
context.RunClaimActions();
4856

4957
await Events.CreatingTicket(context);
5058
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
5159
}
5260

61+
/// <summary>
62+
/// Extracts the claims from the token received from the token endpoint.
63+
/// </summary>
64+
/// <param name="token">The token to extract the claims from.</param>
65+
/// <returns>
66+
/// An <see cref="IEnumerable{Claim}"/> containing the claims extracted from the token.
67+
/// </returns>
68+
protected virtual IEnumerable<Claim> ExtractClaimsFromToken([NotNull] string token)
69+
{
70+
try
71+
{
72+
var securityToken = Options.SecurityTokenHandler.ReadJsonWebToken(token);
73+
74+
var nameClaim = ExtractClaim(securityToken, "name");
75+
var expClaim = ExtractClaim(securityToken, "exp");
76+
77+
var claims = new List<Claim>(securityToken.Claims);
78+
79+
claims.Add(new Claim(ClaimTypes.NameIdentifier, securityToken.Subject.Replace("CHARACTER:EVE:", string.Empty, StringComparison.OrdinalIgnoreCase), ClaimValueTypes.String, ClaimsIssuer));
80+
claims.Add(new Claim(ClaimTypes.Name, nameClaim.Value, ClaimValueTypes.String, ClaimsIssuer));
81+
claims.Add(new Claim(ClaimTypes.Expiration, UnixTimeStampToDateTime(expClaim.Value), ClaimValueTypes.DateTime, ClaimsIssuer));
82+
83+
var scopes = claims.Where(x => string.Equals(x.Type, "scp", StringComparison.OrdinalIgnoreCase)).ToList();
84+
85+
if (scopes.Count > 0)
86+
{
87+
claims.Add(new Claim(EVEOnlineAuthenticationConstants.Claims.Scopes, string.Join(' ', scopes.Select(x => x.Value)), ClaimValueTypes.String, ClaimsIssuer));
88+
}
89+
90+
return claims;
91+
}
92+
catch (Exception ex)
93+
{
94+
throw new InvalidOperationException("Failed to parse JWT for claims from EVEOnline token.", ex);
95+
}
96+
}
97+
98+
private static Claim ExtractClaim([NotNull] JsonWebToken token, [NotNull] string claim)
99+
{
100+
var extractedClaim = token.Claims.FirstOrDefault(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase));
101+
102+
if (extractedClaim == null)
103+
{
104+
throw new InvalidOperationException($"The claim '{claim}' is missing from the EVEOnline JWT.");
105+
}
106+
107+
return extractedClaim;
108+
}
109+
110+
private static string UnixTimeStampToDateTime(string unixTimeStamp)
111+
{
112+
if (!long.TryParse(unixTimeStamp, NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime))
113+
{
114+
throw new InvalidOperationException($"The value {unixTimeStamp} of the 'exp' claim is not a valid 64-bit integer.");
115+
}
116+
117+
DateTimeOffset offset = DateTimeOffset.FromUnixTimeSeconds(unixTime);
118+
return offset.ToString("o", CultureInfo.InvariantCulture);
119+
}
120+
53121
private static partial class Log
54122
{
55123
internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)

src/AspNet.Security.OAuth.EVEOnline/EVEOnlineAuthenticationOptions.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
* for more information concerning the license and the contributors participating to this project.
55
*/
66

7-
using System.Security.Claims;
8-
using static AspNet.Security.OAuth.EVEOnline.EVEOnlineAuthenticationConstants;
7+
using Microsoft.IdentityModel.JsonWebTokens;
98

109
namespace AspNet.Security.OAuth.EVEOnline;
1110

@@ -20,13 +19,13 @@ public EVEOnlineAuthenticationOptions()
2019
CallbackPath = EVEOnlineAuthenticationDefaults.CallbackPath;
2120

2221
Server = EVEOnlineAuthenticationServer.Tranquility;
23-
24-
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "CharacterID");
25-
ClaimActions.MapJsonKey(ClaimTypes.Name, "CharacterName");
26-
ClaimActions.MapJsonKey(ClaimTypes.Expiration, "ExpiresOn");
27-
ClaimActions.MapJsonKey(Claims.Scopes, "Scopes");
2822
}
2923

24+
/// <summary>
25+
/// Gets or sets the optional <see cref="JsonWebTokenHandler"/> to use.
26+
/// </summary>
27+
public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;
28+
3029
/// <summary>
3130
/// Sets the server used when communicating with EVE Online
3231
/// (by default, <see cref="EVEOnlineAuthenticationServer.Tranquility"/>).
@@ -40,13 +39,11 @@ public EVEOnlineAuthenticationServer Server
4039
case EVEOnlineAuthenticationServer.Tranquility:
4140
AuthorizationEndpoint = EVEOnlineAuthenticationDefaults.Tranquility.AuthorizationEndpoint;
4241
TokenEndpoint = EVEOnlineAuthenticationDefaults.Tranquility.TokenEndpoint;
43-
UserInformationEndpoint = EVEOnlineAuthenticationDefaults.Tranquility.UserInformationEndpoint;
4442
break;
4543

4644
case EVEOnlineAuthenticationServer.Singularity:
4745
AuthorizationEndpoint = EVEOnlineAuthenticationDefaults.Singularity.AuthorizationEndpoint;
4846
TokenEndpoint = EVEOnlineAuthenticationDefaults.Singularity.TokenEndpoint;
49-
UserInformationEndpoint = EVEOnlineAuthenticationDefaults.Singularity.UserInformationEndpoint;
5047
break;
5148

5249
default:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using Microsoft.Extensions.Options;
8+
using Microsoft.IdentityModel.JsonWebTokens;
9+
10+
namespace AspNet.Security.OAuth.EVEOnline;
11+
12+
/// <summary>
13+
/// Used to setup defaults for all <see cref="EVEOnlineAuthenticationOptions"/>.
14+
/// </summary>
15+
public class EVEOnlinePostConfigureOptions : IPostConfigureOptions<EVEOnlineAuthenticationOptions>
16+
{
17+
/// <inheritdoc />
18+
public void PostConfigure(
19+
[NotNull] string name,
20+
[NotNull] EVEOnlineAuthenticationOptions options)
21+
{
22+
if (options.SecurityTokenHandler == null)
23+
{
24+
options.SecurityTokenHandler = new JsonWebTokenHandler();
25+
}
26+
}
27+
}

test/AspNet.Security.OAuth.Providers.Tests/EVEOnline/EVEOnlineTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
33
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
44
* for more information concerning the license and the contributors participating to this project.

test/AspNet.Security.OAuth.Providers.Tests/EVEOnline/bundle.json

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,14 @@
22
"$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
33
"items": [
44
{
5-
"uri": "https://login.eveonline.com/oauth/token",
5+
"uri": "https://login.eveonline.com/v2/oauth/token",
66
"method": "POST",
77
"contentFormat": "json",
88
"contentJson": {
9-
"access_token": "secret-access-token",
10-
"token_type": "access",
11-
"refresh_token": "secret-refresh-token",
12-
"expires_in": "300"
13-
}
14-
},
15-
{
16-
"uri": "https://login.eveonline.com/oauth/verify",
17-
"contentFormat": "json",
18-
"contentJson": {
19-
"CharacterID": "my-id",
20-
"CharacterName": "John Smith",
21-
"ExpiresOn": "2019-12-31T23:59:59+00:00",
22-
"Scopes": "my-scopes"
9+
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY3AiOlsibXktc2NvcGVzIl0sImp0aSI6Ijk5OGUxMmM3LTMyNDEtNDNjNS04MzU1LTJjNDg4MjJlMGExYiIsImtpZCI6IkpXVC1TaWduYXR1cmUtS2V5Iiwic3ViIjoiQ0hBUkFDVEVSOkVWRTpteS1pZCIsImF6cCI6Im15M3JkcGFydHljbGllbnRpZCIsIm5hbWUiOiJKb2huIFNtaXRoIiwib3duZXIiOiI4UG16Q2VUS2I0VkZVRHJITGMvQWVaWERTV009IiwiZXhwIjoxNTc3ODM2Nzk5LCJpc3MiOiJsb2dpbi5ldmVvbmxpbmUuY29tIn0._0ziOjbFbJY2Mo9BEaJmEEBKjiKSj1IGIbvbdBH_1Vw",
10+
"refresh_token": "sdfsdfsdfwd",
11+
"expires_in": "1199",
12+
"token_type": "Bearer"
2313
}
2414
}
2515
]

0 commit comments

Comments
 (0)