protected override async Task <AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = Options.TokenRetriever(Context.Request);

            if (token.IsMissing())
            {
                return(AuthenticateResult.Skip());
            }

            if (token.Contains('.') && Options.SkipTokensWithDots)
            {
                _logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
                return(AuthenticateResult.Skip());
            }

            // Resolve request generator
            _requestGenerator = Context.RequestServices.GetService <IIntrospectionRequestGenerator>() ?? new DefaultIntrospectionRequestGenerator();

            if (Options.EnableCaching)
            {
                var claims = await _cache.GetClaimsAsync(token).ConfigureAwait(false);

                if (claims != null && await _requestGenerator.UseCacheAsync(Context.Request, claims))
                {
                    var ticket = CreateTicket(claims);

                    _logger.LogTrace("Token found in cache.");

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    return(AuthenticateResult.Success(ticket));
                }

                _logger.LogTrace("Token is not cached, or the introspection response generator rejected the currently cached claims.");
            }

            // Use a LazyAsync to ensure only one thread is requesting introspection for a token - the rest will wait for the result
            var lazyIntrospection = _lazyTokenIntrospections.GetOrAdd(token, CreateLazyIntrospection);

            try
            {
                var response = await lazyIntrospection.Value.ConfigureAwait(false);

                if (response.IsError)
                {
                    _logger.LogError("Error returned from introspection endpoint: " + response.Error);
                    return(AuthenticateResult.Fail("Error returned from introspection endpoint: " + response.Error));
                }

                if (response.IsActive)
                {
                    var ticket = CreateTicket(response.Claims);

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    if (Options.EnableCaching)
                    {
                        await _cache.SetClaimsAsync(token, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
                    }

                    return(AuthenticateResult.Success(ticket));
                }
                else
                {
                    return(AuthenticateResult.Fail("Token is not active."));
                }
            }
            finally
            {
                // If caching is on and it succeeded, the claims are now in the cache.
                // If caching is off and it succeeded, the claims will be discarded.
                // Either way, we want to remove the temporary store of claims for this token because it is only intended for de-duping fetch requests
                AsyncLazy <IntrospectionResponse> removed;
                _lazyTokenIntrospections.TryRemove(token, out removed);
            }
        }
        /// <summary>
        /// Tries to authenticate a reference token on the current request
        /// </summary>
        /// <returns></returns>
        protected override async Task <AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = Options.TokenRetriever(Context.Request);

            if (token.IsMissing())
            {
                return(AuthenticateResult.NoResult());
            }

            if (token.Contains('.') && Options.SkipTokensWithDots)
            {
                _logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
                return(AuthenticateResult.NoResult());
            }

            if (Options.EnableCaching)
            {
                var claims = await _cache.GetClaimsAsync(token).ConfigureAwait(false);

                if (claims != null)
                {
                    var ticket = CreateTicket(claims);

                    _logger.LogTrace("Token found in cache.");

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    return(AuthenticateResult.Success(ticket));
                }

                _logger.LogTrace("Token is not cached.");
            }

            // Use a LazyAsync to ensure only one thread is requesting introspection for a token - the rest will wait for the result
            var lazyIntrospection = Options.LazyIntrospections.GetOrAdd(token, CreateLazyIntrospection);
            var lazyUserinfo      = Options.LazyUserinfos.GetOrAdd(token, CreateLazyUserinfos);

            try
            {
                var introspectionResponse = await lazyIntrospection.Value.ConfigureAwait(false);

                if (introspectionResponse.IsError)
                {
                    _logger.LogError("Error returned from introspection endpoint: " + introspectionResponse.Error);
                    return(AuthenticateResult.Fail("Error returned from introspection endpoint: " + introspectionResponse.Error));
                }

                if (introspectionResponse.IsActive)
                {
                    var claims = introspectionResponse.Claims;

                    if (Options.GetClaimsFromUserinfoEndpoint)
                    {
                        var userinfoResponse = await lazyUserinfo.Value.ConfigureAwait(false);

                        if (userinfoResponse.IsError)
                        {
                            _logger.LogError("Error returned from userinfo endpoint: " + userinfoResponse.Error);
                            return(AuthenticateResult.Fail("Error returned from userinfo endpoint: " + userinfoResponse.Error));
                        }

                        claims = claims.Union(userinfoResponse.Claims);
                    }

                    if (Options.RoleClaimValueSplitting)
                    {
                        var roleClaimQuery = from c in claims
                                             where c.Type == Options.RoleClaimType
                                             select c;
                        if (roleClaimQuery.Count() == 1)
                        {
                            var roleClaim     = roleClaimQuery.Single();
                            var newRoleClaims = SplitClaim(roleClaim, Options.RoleClaimValueSplittingChar);
                            claims = claims.Union(newRoleClaims);
                        }
                    }

                    var ticket = CreateTicket(claims);

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    if (Options.EnableCaching)
                    {
                        await _cache.SetClaimsAsync(token, claims, Options.CacheDuration, _logger).ConfigureAwait(false);
                    }

                    return(AuthenticateResult.Success(ticket));
                }
                else
                {
                    return(AuthenticateResult.Fail("Token is not active."));
                }
            }
            finally
            {
                // If caching is on and it succeeded, the claims are now in the cache.
                // If caching is off and it succeeded, the claims will be discarded.
                // Either way, we want to remove the temporary store of claims for this token because it is only intended for de-duping fetch requests
                Options.LazyIntrospections.TryRemove(token, out _);
                Options.LazyUserinfos.TryRemove(token, out _);
            }
        }
Example #3
0
        /// <summary>
        /// Tries to authenticate a reference token on the current request
        /// </summary>
        /// <returns></returns>
        protected override async Task <AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = Options.TokenRetriever(Context.Request);

            if (token.IsMissing())
            {
                return(AuthenticateResult.NoResult());
            }

            if (token.Contains('.') && Options.SkipTokensWithDots)
            {
                _logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
                return(AuthenticateResult.NoResult());
            }

            if (Options.EnableCaching)
            {
                var key    = $"{Options.CacheKeyPrefix}{token}";
                var claims = await _cache.GetClaimsAsync(key).ConfigureAwait(false);

                if (claims != null)
                {
                    // find out if it is a cached inactive token
                    var isInActive = claims.FirstOrDefault(c => string.Equals(c.Type, "active", StringComparison.OrdinalIgnoreCase) && string.Equals(c.Value, "false", StringComparison.OrdinalIgnoreCase));
                    if (isInActive != null)
                    {
                        return(await ReportNonSuccessAndReturn("Cached token is not active."));
                    }

                    return(await CreateTicket(claims, token));
                }

                _logger.LogTrace("Token is not cached.");
            }

            // Use a LazyAsync to ensure only one thread is requesting introspection for a token - the rest will wait for the result
            var lazyIntrospection = Options.LazyIntrospections.GetOrAdd(token, CreateLazyIntrospection);

            try
            {
                var response = await lazyIntrospection.Value.ConfigureAwait(false);

                if (response.IsError)
                {
                    _logger.LogError("Error returned from introspection endpoint: " + response.Error);
                    return(await ReportNonSuccessAndReturn("Error returned from introspection endpoint: " + response.Error));
                }

                if (response.IsActive)
                {
                    if (Options.EnableCaching)
                    {
                        var key = $"{Options.CacheKeyPrefix}{token}";
                        await _cache.SetClaimsAsync(key, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
                    }

                    return(await CreateTicket(response.Claims, token));
                }
                else
                {
                    if (Options.EnableCaching)
                    {
                        var key = $"{Options.CacheKeyPrefix}{token}";

                        // add an exp claim - otherwise caching will not work
                        var claimsWithExp = response.Claims.ToList();
                        claimsWithExp.Add(new Claim("exp", DateTimeOffset.UtcNow.Add(Options.CacheDuration).ToUnixTimeSeconds().ToString()));
                        await _cache.SetClaimsAsync(key, claimsWithExp, Options.CacheDuration, _logger).ConfigureAwait(false);
                    }

                    return(await ReportNonSuccessAndReturn("Token is not active."));
                }
            }
            finally
            {
                // If caching is on and it succeeded, the claims are now in the cache.
                // If caching is off and it succeeded, the claims will be discarded.
                // Either way, we want to remove the temporary store of claims for this token because it is only intended for de-duping fetch requests
                Options.LazyIntrospections.TryRemove(token, out _);
            }
        }
Example #4
0
        /// <summary>
        /// Tries to authenticate a reference token on the current request
        /// </summary>
        /// <returns></returns>
        protected override async Task <AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = Options.TokenRetriever(Context.Request);

            if (token.IsMissing())
            {
                return(ReportNonSuccessAndReturn(AuthenticateResult.NoResult()));
            }

            if (token.Contains(".") && Options.SkipTokensWithDots)
            {
                _logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
                return(ReportNonSuccessAndReturn(AuthenticateResult.NoResult()));
            }

            if (Options.EnableCaching)
            {
                var key    = $"{Options.CacheKeyPrefix}{token}";
                var claims = await _cache.GetClaimsAsync(key).ConfigureAwait(false);

                if (claims.Any())
                {
                    var ticket = await CreateTicket(claims);

                    _logger.LogTrace("Token found in cache.");

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    return(AuthenticateResult.Success(ticket));
                }

                _logger.LogTrace("Token is not cached.");
            }

            // Use a LazyAsync to ensure only one thread is requesting introspection for a token - the rest will wait for the result
            var lazyIntrospection = Options.LazyIntrospections.GetOrAdd(token, CreateLazyIntrospection);

            try
            {
                var response = await lazyIntrospection.Value.ConfigureAwait(false);

                if (response.IsError)
                {
                    _logger.LogError("Error returned from introspection endpoint: " + response.Error);
                    return(ReportNonSuccessAndReturn(AuthenticateResult.Fail("Error returned from introspection endpoint: " + response.Error)));
                }

                if (response.IsActive)
                {
                    var ticket = await CreateTicket(response.Claims);

                    if (Options.SaveToken)
                    {
                        ticket.Properties.StoreTokens(new[]
                        {
                            new AuthenticationToken {
                                Name = "access_token", Value = token
                            }
                        });
                    }

                    if (Options.EnableCaching)
                    {
                        var key = $"{Options.CacheKeyPrefix}{token}";
                        await _cache.SetClaimsAsync(key, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
                    }

                    return(AuthenticateResult.Success(ticket));
                }
                else
                {
                    return(ReportNonSuccessAndReturn(AuthenticateResult.Fail("Token is not active.")));
                }
            }
            finally
            {
                // If caching is on and it succeeded, the claims are now in the cache.
                // If caching is off and it succeeded, the claims will be discarded.
                // Either way, we want to remove the temporary store of claims for this token because it is only intended for de-duping fetch requests
                Options.LazyIntrospections.TryRemove(token, out _);
            }
        }
        /// <summary>
        /// Tries to authenticate a reference token on the current request
        /// </summary>
        /// <returns></returns>
        protected override async Task <AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = Options.TokenRetriever(Context.Request);

            // no token - nothing to do here
            if (token.IsMissing())
            {
                return(AuthenticateResult.NoResult());
            }

            // if token contains a dot - it might be a JWT and we are skipping
            // this is configurable
            if (token.Contains('.') && Options.SkipTokensWithDots)
            {
                _logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
                return(AuthenticateResult.NoResult());
            }

            // if caching is enable - let's check if we have a cached introspection
            if (Options.EnableCaching)
            {
                var key    = $"{Options.CacheKeyPrefix}{token}";
                var claims = await _cache.GetClaimsAsync(key).ConfigureAwait(false);

                if (claims != null)
                {
                    // find out if it is a cached inactive token
                    var isInActive = claims.FirstOrDefault(c => string.Equals(c.Type, "active", StringComparison.OrdinalIgnoreCase) && string.Equals(c.Value, "false", StringComparison.OrdinalIgnoreCase));
                    if (isInActive != null)
                    {
                        return(await ReportNonSuccessAndReturn("Cached token is not active."));
                    }

                    return(await CreateTicket(claims, token));
                }

                _logger.LogTrace("Token is not cached.");
            }

            // no cached result - let's make a network roundtrip to the introspection endpoint
            // this code block tries to make sure that we only do a single roundtrip, even when multiple requests
            // with the same token come in at the same time
            try
            {
                return(await IntrospectionDictionary.GetOrAdd(token, _ =>
                {
                    return new Lazy <Task <AuthenticateResult> >(async() =>
                    {
                        var response = await LoadClaimsForToken(token);

                        if (response.IsError)
                        {
                            _logger.LogError("Error returned from introspection endpoint: " + response.Error);
                            return await ReportNonSuccessAndReturn("Error returned from introspection endpoint: " + response.Error);
                        }

                        if (response.IsActive)
                        {
                            if (Options.EnableCaching)
                            {
                                var key = $"{Options.CacheKeyPrefix}{token}";
                                await _cache.SetClaimsAsync(key, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
                            }

                            return await CreateTicket(response.Claims, token);
                        }
                        else
                        {
                            if (Options.EnableCaching)
                            {
                                var key = $"{Options.CacheKeyPrefix}{token}";

                                // add an exp claim - otherwise caching will not work
                                var claimsWithExp = response.Claims.ToList();
                                claimsWithExp.Add(new Claim("exp",
                                                            DateTimeOffset.UtcNow.Add(Options.CacheDuration).ToUnixTimeSeconds().ToString()));
                                await _cache.SetClaimsAsync(key, claimsWithExp, Options.CacheDuration, _logger)
                                .ConfigureAwait(false);
                            }

                            return await ReportNonSuccessAndReturn("Token is not active.");
                        }
                    });
                }).Value);
            }
            finally
            {
                IntrospectionDictionary.TryRemove(token, out _);
            }
        }