Attività di "hitaspdotnet"

  • MOVED FROM: here
  • ABP Framework version: v8.1.0
  • UI Type: MVC
  • Database System: MongoDB
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes/ ABP Studio Microservice
  • Exception message and full stack trace: N/A
  • Steps to reproduce the issue: N/A

Hi dear support team. I need a custom authorization flow on my ABP application please guide me. I am very confused.

I have a web application that is hosted by Telegram Web App. This is a React application implemented in NextJS. The only thing I have access to is the token that Telegram produces. It gives me a hash string and its decryption key to validate that this request was sent by a user in Telegram. I want to auto-register every user who opened my Telegram app if they are not registered before. After confirming the token sent by Telegram, which has a userId, create a user with their Telegram ID as username and give them default roles. I also want users to access their account on the dashboard, which is the ABP admin web, without the need for a password using the Telegram Login widget, it has the same algorithm and I want to authenticate users using telegram token. What is the best flow I can use to handle this in ABP Studio MS Template (openid) please?

here is an overview of the data I can use to validate request:

using System;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Linq;


namespace Telegram
{
    /// <summary>
    /// A helper class used to verify authorization data
    /// </summary>
    public class LoginWidget : IDisposable
    {
        /// <summary>
        /// How old (in seconds) can authorization attempts be to be considered valid (compared to the auth_date field)
        /// </summary>
        public long AllowedTimeOffset = 30;

        private bool _disposed = false;
        private readonly HMACSHA256 _hmac;
        private static readonly DateTime _unixStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// Construct a new <see cref="LoginWidget"/> instance
        /// </summary>
        /// <param name="token">The bot API token used as a secret parameter when checking authorization</param>
        public LoginWidget(string token)
        {
            if (token == null) throw new ArgumentNullException(nameof(token));

            using (SHA256 sha256 = SHA256.Create())
            {
                _hmac = new HMACSHA256(sha256.ComputeHash(Encoding.ASCII.GetBytes(token)));
            }
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as sorted key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(SortedDictionary<string, string> fields)
        {
            if (_disposed) throw new ObjectDisposedException(nameof(LoginWidget));
            if (fields == null) throw new ArgumentNullException(nameof(fields));
            if (fields.Count < 3) return Authorization.MissingFields;

            if (!fields.ContainsKey(Field.Id) ||
                !fields.TryGetValue(Field.AuthDate, out string authDate) ||
                !fields.TryGetValue(Field.Hash, out string hash)
            ) return Authorization.MissingFields;

            if (hash.Length != 64) return Authorization.InvalidHash;

            if (!long.TryParse(authDate, out long timestamp))
                return Authorization.InvalidAuthDateFormat;

            if (Math.Abs(DateTime.UtcNow.Subtract(_unixStart).TotalSeconds - timestamp) > AllowedTimeOffset)
                return Authorization.TooOld;

            fields.Remove(Field.Hash);
            StringBuilder dataStringBuilder = new StringBuilder(256);
            foreach (var field in fields)
            {
                if (!string.IsNullOrEmpty(field.Value))
                {
                    dataStringBuilder.Append(field.Key);
                    dataStringBuilder.Append('=');
                    dataStringBuilder.Append(field.Value);
                    dataStringBuilder.Append('\n');
                }
            }
            dataStringBuilder.Length -= 1; // Remove the last \n

            byte[] signature = _hmac.ComputeHash(Encoding.UTF8.GetBytes(dataStringBuilder.ToString()));

            // Adapted from: https://stackoverflow.com/a/14333437/6845657
            for (int i = 0; i < signature.Length; i++)
            {
                if (hash[i * 2] != 87 + (signature[i] >> 4) + ((((signature[i] >> 4) - 10) >> 31) & -39)) return Authorization.InvalidHash;
                if (hash[i * 2 + 1] != 87 + (signature[i] & 0xF) + ((((signature[i] & 0xF) - 10) >> 31) & -39)) return Authorization.InvalidHash;
            }

            return Authorization.Valid;
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(Dictionary<string, string> fields)
        {
            if (fields == null) throw new ArgumentNullException(nameof(fields));
            return CheckAuthorization(new SortedDictionary<string, string>(fields, StringComparer.Ordinal));
        }

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(IEnumerable<KeyValuePair<string, string>> fields) =>
            CheckAuthorization(fields?.ToDictionary(f => f.Key, f => f.Value, StringComparer.Ordinal));

        /// <summary>
        /// Checks whether the authorization data received from the user is valid
        /// </summary>
        /// <param name="fields">A collection containing query string fields as key-value pairs</param>
        /// <returns></returns>
        public Authorization CheckAuthorization(IEnumerable<Tuple<string, string>> fields) =>
            CheckAuthorization(fields?.ToDictionary(f => f.Item1, f => f.Item2, StringComparer.Ordinal));

        public void Dispose()
        {
            if (!_disposed)
            {
                _disposed = true;
                _hmac?.Dispose();
            }
        }

        private static class Field
        {
            public const string AuthDate = "auth_date";
            public const string Id = "id";
            public const string Hash = "hash";
        }
    }
    
    public enum Authorization
    {
        InvalidHash,
        MissingFields,
        InvalidAuthDateFormat,
        TooOld,
        Valid
    }
}

For points 3 and 4 we do not have Domain, Application Contracts and Test solutions in Microservice template. So I am not sure whether do I put them?

If it's a ABP Studio template ABP Studio's microservice template has single layer services (actually two layers: host and contracts but they named it single layer), it's designed to host modules. you should create your own modules and add their dependencies to the related services: YourModule.Application.Contracts -> YourService.Contracts YourModule.[DB_PROVIDER] & YourModule.HttpApi & YourModule.Application -> YourService YourService.Contracts & YourModule.HttpApi.Client & YourModule.Web -> apps/web

Also when creating the project using microservice template , I am getting many errors while opening the project.

you should build your solution via abp studio or run this command in these folders apps/authserver + apps/web + apps/public dotnet build /graphbuild

hi

You can check this: https://github.com/abpframework/abp/issues/4977#issuecomment-670006297

Add your TelegramExternalLoginProvider

let me try to be clearer. this is a telegram web application. as you see, telegram user already detected. I used same algorithm but client side (nextjs - react). I need to authorize these users and their permissions in backend. telegram web app has force dependency to telegram bot, so we have to create a bot if we want to launch a telegram web app. so, I decided to mark bots as our tenants and app's users as tenant users. I created a custom tenant resolver to get bot ID from request. Also, I created a custom grant_type and I added it to bot client's scops to accept requests from the bot. I need an endpoint to post data: auth_data, id, hash and get token to store it in the client storage and making queries to the API using this token based on their permissions. if received data is valid but requester user is not existed in ABP user db then should create it with default role and not be rejected, then generate and send token.

I also have a criticism; I have been a fan of ABP team since ZERO and have always followed your work because of my interest in your team. But the support forum is more like GitHub issue and only the bugs of the framework itself are well supported.

So far, the framework's identity module has met all the infrastructure I needed, and I never had to customize it. I have no idea where to start. I'm not asking you for code, I just want to know where I should go to customize ABP authentication for this or any similar scenarios. I appreciate your guidance.

Best regards.

Thank you @maliming. You are awesome. is it best practice to add whole process inside custom grant type?

I have a host-side entity to store keys for hash decryption: key = $"{ICurrrentTenant.Name}:{TelegramBotApiToken}". tenant name is same as bot username. here are steps I need to do to grant a request:

  1. Get telegram data from request parameters
  2. Inject keyRepository and get current tenant's key
  3. validate hash from step 1
  4. get user's userName from telegram data
  5. check if already existed else create it and assign default roles.
  6. create principals
  7. set scops, claims and resources
  8. handle principal and log security
  9. return sign in result

then the token and refresh token are accessible at /connect/token?

hi

is it best practice to add whole process inside custom grant type?

I tend to do this.

then the token and refresh token are accessible at /connect/token?

Yes, Because your access_token is legal.

You can add some simple logic checks in your custom grant type. and then integrate Telegram.

Thank you very much sir. You gave me a very clear idea. I decided to implement user creator in a separate service. First, I will search for the identityUser via loginInfo by the provider infos, if it is not found, I will reject the request in custom grant type. Then I check the response in the client side, if the request is rejected, I'll send a request with telegram user's info to the semi-fake user creator service.

  • ABP Framework version: v8.1.0-rc.1
  • UI Type: MVC
  • Database System: MongoDB
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes/ ABP Studio Microservice
  • Exception message and full stack trace: N/A
  • Steps to reproduce the issue: N/A

Hi, I added subdomain tenant resolver to my abp application. everything works except "login as this tenant" and user menus "Security Log" & "My Account" has invalid url (actually I fixed it with {0} replacer). When I trying to impersonate tenant from host dashboard it's just redirect to the host home page. I think this is related to the tenant resolver. I expected it to impersonate the tenant on the host domain. Do we have a list of resolvers? Can we fix this by resolvers ordering?

Hi,

How do you configure the subdomain tenant resolver?

Could you share your steps?

in web, web.public & authserver I added:

Configure<AbpTenantResolveOptions>(options =>
{
    options.AddDomainTenantResolver(configuration["TenantDomain"]);
});

also this config in their appSettings:

"TenantDomain": "https://{0}.app.domain.com"
...
"TenantDomain": "https://{0}.pub.domain.com"
...
"TenantDomain": "https://{0}.auth.domain.com"

in authserver module:

PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
    options.EnableWildcardDomainSupport = true;
    options.WildcardDomainsFormat.Add(configuration["WildCardDomains:AuthServer"]);
    options.WildcardDomainsFormat.Add(configuration["WildCardDomains:Web"]);
    options.WildcardDomainsFormat.Add(configuration["WildCardDomains:PublicWeb"]);
});

and authserver's appSettings:

"WildCardDomains": {
    "AuthServer": "https://{0}.auth.domain.com",
    "Web": "https://{0}.app.domain.com",
    "PublicWeb": "https://{0}.pub.domain.com"
}

then in web & web.public :

if (Convert.ToBoolean(configuration["AuthServer:IsOnK8s"]))
{
    context.Services.Configure<OpenIdConnectOptions>(
        "oidc", options =>
        {
            options.MetadataAddress = configuration["AuthServer:MetaAddress"]!.EnsureEndsWith('/') +
                                      ".well-known/openid-configuration";

            var previousOnRedirectToIdentityProvider = options.Events.OnRedirectToIdentityProvider;
            options.Events.OnRedirectToIdentityProvider = async ctx =>
            {
                ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"]!.EnsureEndsWith('/') + "connect/authorize";

                var currentTenant = ctx.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
                var tenantDomain = configuration["TenantDomain"];

                if (currentTenant.IsAvailable && !string.IsNullOrEmpty(tenantDomain))
                {
                    ctx.ProtocolMessage.IssuerAddress =
                        ctx.ProtocolMessage.IssuerAddress.Replace("{0}", $"{currentTenant.Name}");
                }
                else
                {
                    ctx.ProtocolMessage.IssuerAddress =
                        ctx.ProtocolMessage.IssuerAddress.Replace("{0}.", string.Empty);
                }

                if (previousOnRedirectToIdentityProvider != null)
                {
                    await previousOnRedirectToIdentityProvider(ctx);
                }
            };

            var previousOnRedirectToIdentityProviderForSignOut =
                options.Events.OnRedirectToIdentityProviderForSignOut;
            options.Events.OnRedirectToIdentityProviderForSignOut = async ctx =>
            {
                // Intercept the redirection for signout so the browser navigates to the right URL in your host
                ctx.ProtocolMessage.IssuerAddress = configuration["AuthServer:Authority"]!.EnsureEndsWith('/') +
                                                    "connect/logout";

                var currentTenant = ctx.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
                var tenantDomain = configuration["TenantDomain"];
                if (currentTenant.IsAvailable &&
                    !string.IsNullOrEmpty(tenantDomain))
                {
                    ctx.ProtocolMessage.IssuerAddress =
                        ctx.ProtocolMessage.IssuerAddress.Replace("{0}", $"{currentTenant.Name}");
                }
                else
                {
                    ctx.ProtocolMessage.IssuerAddress =
                        ctx.ProtocolMessage.IssuerAddress.Replace("{0}.", string.Empty);
                }

                if (previousOnRedirectToIdentityProviderForSignOut != null)
                {
                    await previousOnRedirectToIdentityProviderForSignOut(ctx);
                }
            };
        }
    );
}

and the userMenu in web & web.public

var authServerUrl = _configuration["AuthServer:Authority"] ?? "~";
var returnUrl = _configuration["App:SelfUrl"] ?? "";

var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
var tenantDomain = _configuration["TenantDomain"];

if (currentTenant.IsAvailable && !string.IsNullOrEmpty(tenantDomain))
{
    authServerUrl = authServerUrl.Replace("{0}", $"{currentTenant.Name}");
    returnUrl = tenantDomain.Replace("{0}", $"{currentTenant.Name}");
}
else
{
    authServerUrl = authServerUrl.Replace("{0}.", string.Empty);
}

I kept gateways and services as before except openIddict data seeder.

is it possible to impersonate tenant on the host domain? If yes, please guide me!

Do I need to config AuthServer:Authority in authServer too? After clicking on Login I see redirect to auth.domain.com which is host's issuer and then redirected to host home page. the url after click on login:

https://auth.tonner.io/connect/authorize?client_id=Web&redirect_uri=https%3A%2F%2Fapp.tonner.io%2Fsignin-oidc&response_type=code%20id_token&scope=<scopes>&response_mode=form_post&nonce=<nonce>&access_token=<token>&tenantid=bec78466-c1c3-d8dc-beb3-3a112a214a25&tenantusername=admin&returnurl=https%3A%2F%2Fapp.tonner.io%2FSaas%2FHost%2FTenants&state=<state>&x-client-SKU=ID_NET8_0&x-client-ver=7.0.3.0
51 - 60 di 63
Made with ❤️ on ABP v8.2.0-preview Updated on marzo 25, 2024, 15:11