// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Antiforgery
{
    internal class DefaultAntiforgeryTokenGenerator : IAntiforgeryTokenGenerator
    {
        private readonly IClaimUidExtractor _claimUidExtractor;
        private readonly IAntiforgeryAdditionalDataProvider _additionalDataProvider;

        public DefaultAntiforgeryTokenGenerator(
            IClaimUidExtractor claimUidExtractor,
            IAntiforgeryAdditionalDataProvider additionalDataProvider)
        {
            _claimUidExtractor = claimUidExtractor;
            _additionalDataProvider = additionalDataProvider;
        }

        /// <inheritdoc />
        public AntiforgeryToken GenerateCookieToken()
        {
            return new AntiforgeryToken()
            {
                // SecurityToken will be populated automatically.
                IsCookieToken = true
            };
        }

        /// <inheritdoc />
        public AntiforgeryToken GenerateRequestToken(
            HttpContext httpContext,
            AntiforgeryToken cookieToken)
        {
            if (httpContext == null)
            {
                throw new ArgumentNullException(nameof(httpContext));
            }

            if (cookieToken == null)
            {
                throw new ArgumentNullException(nameof(cookieToken));
            }

            if (!IsCookieTokenValid(cookieToken))
            {
                throw new ArgumentException(
                    Resources.Antiforgery_CookieToken_IsInvalid,
                    nameof(cookieToken));
            }

            var requestToken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                IsCookieToken = false
            };

            var isIdentityAuthenticated = false;

            // populate Username and ClaimUid
            var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
            if (authenticatedIdentity != null)
            {
                isIdentityAuthenticated = true;
                requestToken.ClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));

                if (requestToken.ClaimUid == null)
                {
                    requestToken.Username = authenticatedIdentity.Name;
                }
            }

            // populate AdditionalData
            if (_additionalDataProvider != null)
            {
                requestToken.AdditionalData = _additionalDataProvider.GetAdditionalData(httpContext);
            }

            if (isIdentityAuthenticated
                && string.IsNullOrEmpty(requestToken.Username)
                && requestToken.ClaimUid == null
                && string.IsNullOrEmpty(requestToken.AdditionalData))
            {
                // Application says user is authenticated, but we have no identifier for the user.
                throw new InvalidOperationException(
                    Resources.FormatAntiforgeryTokenValidator_AuthenticatedUserWithoutUsername(
                        authenticatedIdentity?.GetType() ?? typeof(ClaimsIdentity),
                        nameof(IIdentity.IsAuthenticated),
                        "true",
                        nameof(IIdentity.Name),
                        nameof(IAntiforgeryAdditionalDataProvider),
                        nameof(DefaultAntiforgeryAdditionalDataProvider)));
            }

            return requestToken;
        }

        /// <inheritdoc />
        public bool IsCookieTokenValid(AntiforgeryToken? cookieToken)
        {
            return cookieToken != null && cookieToken.IsCookieToken;
        }

        /// <inheritdoc />
        public bool TryValidateTokenSet(
            HttpContext httpContext,
            AntiforgeryToken cookieToken,
            AntiforgeryToken requestToken,
            [NotNullWhen(false)] out string? message)
        {
            if (httpContext == null)
            {
                throw new ArgumentNullException(nameof(httpContext));
            }

            if (cookieToken == null)
            {
                throw new ArgumentNullException(
                    nameof(cookieToken),
                    Resources.Antiforgery_CookieToken_MustBeProvided_Generic);
            }

            if (requestToken == null)
            {
                throw new ArgumentNullException(
                    nameof(requestToken),
                    Resources.Antiforgery_RequestToken_MustBeProvided_Generic);
            }

            // Do the tokens have the correct format?
            if (!cookieToken.IsCookieToken || requestToken.IsCookieToken)
            {
                message = Resources.AntiforgeryToken_TokensSwapped;
                return false;
            }

            // Are the security tokens embedded in each incoming token identical?
            if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken))
            {
                message = Resources.AntiforgeryToken_SecurityTokenMismatch;
                return false;
            }

            // Is the incoming token meant for the current user?
            var currentUsername = string.Empty;
            BinaryBlob? currentClaimUid = null;

            var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
            if (authenticatedIdentity != null)
            {
                currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));
                if (currentClaimUid == null)
                {
                    currentUsername = authenticatedIdentity.Name ?? string.Empty;
                }
            }

            // OpenID and other similar authentication schemes use URIs for the username.
            // These should be treated as case-sensitive.
            var comparer = StringComparer.OrdinalIgnoreCase;
            if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
                currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            {
                comparer = StringComparer.Ordinal;
            }

            if (!comparer.Equals(requestToken.Username, currentUsername))
            {
                message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);
                return false;
            }

            if (!object.Equals(requestToken.ClaimUid, currentClaimUid))
            {
                message = Resources.AntiforgeryToken_ClaimUidMismatch;
                return false;
            }

            // Is the AdditionalData valid?
            if (_additionalDataProvider != null &&
                !_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData))
            {
                message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;
                return false;
            }

            message = null;
            return true;
        }

        private static BinaryBlob? GetClaimUidBlob(string? base64ClaimUid)
        {
            if (base64ClaimUid == null)
            {
                return null;
            }

            return new BinaryBlob(256, Convert.FromBase64String(base64ClaimUid));
        }

        private static ClaimsIdentity? GetAuthenticatedIdentity(ClaimsPrincipal? claimsPrincipal)
        {
            if (claimsPrincipal == null)
            {
                return null;
            }

            var identitiesList = claimsPrincipal.Identities as List<ClaimsIdentity>;
            if (identitiesList != null)
            {
                for (var i = 0; i < identitiesList.Count; i++)
                {
                    if (identitiesList[i].IsAuthenticated)
                    {
                        return identitiesList[i];
                    }
                }
            }
            else
            {
                foreach (var identity in claimsPrincipal.Identities)
                {
                    if (identity.IsAuthenticated)
                    {
                        return identity;
                    }
                }
            }

            return null;
        }
    }
}
