11using System ;
22using System . Collections . Generic ;
3+ using System . IdentityModel . Tokens . Jwt ;
34using System . Linq ;
45using System . Security . Claims ;
56using System . Text ;
2021using Microsoft . Extensions . DependencyInjection ;
2122using Microsoft . Extensions . Hosting ;
2223using Microsoft . Extensions . Logging ;
24+ using Microsoft . IdentityModel . Protocols ;
2325using Microsoft . IdentityModel . Protocols . OpenIdConnect ;
2426using Microsoft . IdentityModel . Tokens ;
27+ using Serilog ;
2528using MessageReceivedContext = Microsoft . AspNetCore . Authentication . JwtBearer . MessageReceivedContext ;
2629using TokenValidatedContext = Microsoft . AspNetCore . Authentication . OpenIdConnect . TokenValidatedContext ;
2730
@@ -139,8 +142,51 @@ private static void SetupOpenIdConnectAuthentication(this IServiceCollection ser
139142 var isDevelopment = environment . IsEnvironment ( Environments . Development ) ;
140143 var baseUrl = Configuration . BaseUrl ;
141144
142- var apiPrefix = baseUrl + "api" ;
143- var hubsPrefix = baseUrl + "hubs" ;
145+ const string apiPrefix = "/api" ;
146+ const string hubsPrefix = "/hubs" ;
147+
148+ var authority = Configuration . OidcSettings . Authority ;
149+ if ( ! isDevelopment && ! authority . StartsWith ( "https" ) )
150+ {
151+ Log . Error ( "OpenIdConnect authority is not using https, you must configure tls for your idp." ) ;
152+ return ;
153+ }
154+
155+ var hasTrailingSlash = authority . EndsWith ( '/' ) ;
156+ var url = authority + ( hasTrailingSlash ? string . Empty : "/" ) + ".well-known/openid-configuration" ;
157+
158+ var configurationManager = new ConfigurationManager < OpenIdConnectConfiguration > (
159+ url ,
160+ new OpenIdConnectConfigurationRetriever ( ) ,
161+ new HttpDocumentRetriever { RequireHttps = ! isDevelopment }
162+ ) ;
163+
164+ ICollection < string > supportedScopes ;
165+ try
166+ {
167+ supportedScopes = configurationManager . GetConfigurationAsync ( )
168+ . ConfigureAwait ( false )
169+ . GetAwaiter ( )
170+ . GetResult ( )
171+ . ScopesSupported ;
172+ }
173+ catch ( Exception ex )
174+ {
175+ // Do not interrupt startup if OIDC fails (Network outage should still allow Kavita to run)
176+ Log . Error ( ex , "Failed to load OIDC configuration, OIDC will not be enabled. Restart to retry" ) ;
177+ return ;
178+ }
179+
180+ List < string > scopes = [ "openid" , "profile" , "offline_access" , "roles" , "email" ] ;
181+ scopes . AddRange ( settings . CustomScopes ) ;
182+ var validScopes = scopes . Where ( scope =>
183+ {
184+ if ( supportedScopes . Contains ( scope ) )
185+ return true ;
186+
187+ Log . Warning ( "Scope {Scope} is configured, but not supported by your OIDC provider. Skipping" , scope ) ;
188+ return false ;
189+ } ) . ToList ( ) ;
144190
145191 services . AddOptions < CookieAuthenticationOptions > ( CookieAuthenticationDefaults . AuthenticationScheme ) . Configure < ITicketStore > ( ( options , store ) =>
146192 {
@@ -150,6 +196,7 @@ private static void SetupOpenIdConnectAuthentication(this IServiceCollection ser
150196 options . Cookie . HttpOnly = true ;
151197 options . Cookie . IsEssential = true ;
152198 options . Cookie . MaxAge = TimeSpan . FromDays ( 7 ) ;
199+ options . Cookie . SameSite = SameSiteMode . Strict ;
153200 options . SessionStore = store ;
154201
155202 if ( isDevelopment )
@@ -193,21 +240,25 @@ private static void SetupOpenIdConnectAuthentication(this IServiceCollection ser
193240
194241 options . SaveTokens = true ;
195242 options . GetClaimsFromUserInfoEndpoint = true ;
196- options . Scope . Clear ( ) ;
197- options . Scope . Add ( "openid" ) ;
198- options . Scope . Add ( "profile" ) ;
199- options . Scope . Add ( "offline_access" ) ;
200- options . Scope . Add ( "roles" ) ;
201- options . Scope . Add ( "email" ) ;
202243
203- foreach ( var customScope in settings . CustomScopes )
244+ // Due to some (Authelia) OIDC providers, we need to map these claims explicitly. Such that no flow breaks in the
245+ // OidcService
246+ options . MapInboundClaims = true ;
247+ options . ClaimActions . MapJsonKey ( ClaimTypes . Email , "email" ) ;
248+ options . ClaimActions . MapJsonKey ( ClaimTypes . Name , "name" ) ;
249+ options . ClaimActions . MapJsonKey ( JwtRegisteredClaimNames . PreferredUsername , "preferred_username" ) ;
250+ options . ClaimActions . MapJsonKey ( ClaimTypes . GivenName , "given_name" ) ;
251+
252+ options . Scope . Clear ( ) ;
253+ foreach ( var scope in validScopes )
204254 {
205- options . Scope . Add ( customScope ) ;
255+ options . Scope . Add ( scope ) ;
206256 }
207257
258+
208259 options . Events = new OpenIdConnectEvents
209260 {
210- OnTokenValidated = OidcClaimsPrincipalConverter ,
261+ OnTicketReceived = OidcClaimsPrincipalConverter ,
211262 OnAuthenticationFailed = ctx =>
212263 {
213264 ctx . Response . Redirect ( baseUrl + "login?skipAutoLogin=true&error=" + Uri . EscapeDataString ( ctx . Exception . Message ) ) ;
@@ -252,7 +303,7 @@ private static void SetupOpenIdConnectAuthentication(this IServiceCollection ser
252303 /// Kavita roles the user has
253304 /// </summary>
254305 /// <param name="ctx"></param>
255- private static async Task OidcClaimsPrincipalConverter ( TokenValidatedContext ctx )
306+ private static async Task OidcClaimsPrincipalConverter ( TicketReceivedContext ctx )
256307 {
257308 if ( ctx . Principal == null ) return ;
258309
@@ -264,58 +315,16 @@ private static async Task OidcClaimsPrincipalConverter(TokenValidatedContext ctx
264315 }
265316
266317 var claims = await OidcService . ConstructNewClaimsList ( ctx . HttpContext . RequestServices , ctx . Principal , user ) ;
267- var tokens = CopyOidcTokens ( ctx ) ;
268318
269319 var identity = new ClaimsIdentity ( claims , ctx . Scheme . Name ) ;
270320 var principal = new ClaimsPrincipal ( identity ) ;
271321
272- ctx . Properties ??= new AuthenticationProperties ( ) ;
273- ctx . Properties . StoreTokens ( tokens ) ;
274-
275322 ctx . HttpContext . User = principal ;
276323 ctx . Principal = principal ;
277324
278325 ctx . Success ( ) ;
279326 }
280327
281- /// <summary>
282- /// Copy tokens returned by the OIDC provider that we require later
283- /// </summary>
284- /// <param name="ctx"></param>
285- /// <returns></returns>
286- private static List < AuthenticationToken > CopyOidcTokens ( TokenValidatedContext ctx )
287- {
288- if ( ctx . TokenEndpointResponse == null )
289- {
290- return [ ] ;
291- }
292-
293- var tokens = new List < AuthenticationToken > ( ) ;
294-
295- if ( ! string . IsNullOrEmpty ( ctx . TokenEndpointResponse . RefreshToken ) )
296- {
297- tokens . Add ( new AuthenticationToken { Name = OidcService . RefreshToken , Value = ctx . TokenEndpointResponse . RefreshToken } ) ;
298- }
299- else
300- {
301- var logger = ctx . HttpContext . RequestServices . GetRequiredService < ILogger < OidcService > > ( ) ;
302- logger . LogWarning ( "OIDC login without refresh token, automatic sync will not work for this user" ) ;
303- }
304-
305- if ( ! string . IsNullOrEmpty ( ctx . TokenEndpointResponse . IdToken ) )
306- {
307- tokens . Add ( new AuthenticationToken { Name = OidcService . IdToken , Value = ctx . TokenEndpointResponse . IdToken } ) ;
308- }
309-
310- if ( ! string . IsNullOrEmpty ( ctx . TokenEndpointResponse . ExpiresIn ) )
311- {
312- var expiresAt = DateTimeOffset . UtcNow . AddSeconds ( double . Parse ( ctx . TokenEndpointResponse . ExpiresIn ) ) ;
313- tokens . Add ( new AuthenticationToken { Name = OidcService . ExpiresAt , Value = expiresAt . ToString ( "o" ) } ) ;
314- }
315-
316- return tokens ;
317- }
318-
319328 private static Task SetTokenFromQuery ( MessageReceivedContext context )
320329 {
321330 var accessToken = context . Request . Query [ "access_token" ] ;
0 commit comments