4
4
* for more information concerning the license and the contributors participating to this project.
5
5
*/
6
6
7
- using System . Net . Http . Headers ;
7
+ using System . Globalization ;
8
8
using System . Security . Claims ;
9
9
using System . Text . Encodings . Web ;
10
- using System . Text . Json ;
11
10
using Microsoft . Extensions . Logging ;
12
11
using Microsoft . Extensions . Options ;
12
+ using Microsoft . IdentityModel . JsonWebTokens ;
13
13
14
14
namespace AspNet . Security . OAuth . EVEOnline ;
15
15
16
16
public partial class EVEOnlineAuthenticationHandler : OAuthHandler < EVEOnlineAuthenticationOptions >
17
17
{
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>
18
25
public EVEOnlineAuthenticationHandler (
19
26
[ NotNull ] IOptionsMonitor < EVEOnlineAuthenticationOptions > options ,
20
27
[ NotNull ] ILoggerFactory logger ,
@@ -29,27 +36,88 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
29
36
[ NotNull ] AuthenticationProperties properties ,
30
37
[ NotNull ] OAuthTokenResponse tokens )
31
38
{
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 ;
35
40
36
- using var response = await Backchannel . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , Context . RequestAborted ) ;
37
- if ( ! response . IsSuccessStatusCode )
41
+ if ( string . IsNullOrWhiteSpace ( accessToken ) )
38
42
{
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." ) ;
41
44
}
42
45
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
+ }
44
52
45
53
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 ) ;
47
55
context . RunClaimActions ( ) ;
48
56
49
57
await Events . CreatingTicket ( context ) ;
50
58
return new AuthenticationTicket ( context . Principal ! , context . Properties , Scheme . Name ) ;
51
59
}
52
60
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
+
53
121
private static partial class Log
54
122
{
55
123
internal static async Task UserProfileErrorAsync ( ILogger logger , HttpResponseMessage response , CancellationToken cancellationToken )
0 commit comments