ShopifySharp icon indicating copy to clipboard operation
ShopifySharp copied to clipboard

Add support for validating/decoding embedded app session tokens

Open nozzlegear opened this issue 3 years ago • 6 comments

Now that browsers are starting to block third-party cookies by default, Shopify has added a session token to embedded apps which can be used to validate that a user is authenticated without relying on cookies. Whenever an app is loaded in an embedded context, the querystring will contain the usual hmac stuff along with a session value. The session string is a typical JWT and can be validated/decoded as such.

I plan to add support for this to ShopifySharp's AuthorizationService.

Docs: https://shopify.dev/apps/auth/session-tokens

nozzlegear avatar Jan 17 '22 16:01 nozzlegear

I'm planning on taking on a dependency to Microsoft.IdentityModel.JsonWebTokens instead of implementing the JWT parsing manually.

nozzlegear avatar Jan 18 '22 17:01 nozzlegear

FYI, I use session tokens in my app already and didn't need to decode tokens manually as it is handled by ASP.NET 6 already.

clement911 avatar Feb 14 '22 20:02 clement911

@clement911 I'm with you, after implementing it in a test app I don't think the validation/decoding feature is necessary at all! At best we might be able to provide some kind of default class that the session could be decoded into (e.g. token.dest becomes token.ShopUrl).

nozzlegear avatar Feb 18 '22 17:02 nozzlegear

@clement911 Could you tell me how you use session token in .NET core 6? Thanks Deepak

deepakkumar1984 avatar Apr 19 '22 12:04 deepakkumar1984

@deepakkumar1984 I use the AddJwtBearer method to configure it with my API keys.

Like this:

services.AddJwtBearer("ShopifySession", options =>
                 {
                     options.TokenValidationParameters = new TokenValidationParameters
                     {
                         AuthenticationType = "ShopifySession",
                         IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(shopifyConfig.SecretKey)),
                         ValidAudience = shopifyConfig.ApiKey,
                         ValidateLifetime = true,//ensure not expired
                         ValidateAudience = true,//ensure for correct api key
                         ValidateIssuerSigningKey = true,//ensure for correct secret key
                         ValidateIssuer = false,//issuer is the shop's domain, which we don't yet know
                         ValidateActor = false,//not used by Shopify Session Tokens
                     };
                 })

On the client side (browser), you need to get the session token with AppBridge and set the Authorization HTTP Header to 'Bearer '.

Then the HttpContext.User.Identities should contain an additional ClaimsIdentity.

I used the following method to extract the shop id and user id from the ClaimsIdentity.

public bool TryGetShopifySessionInfo(ClaimsIdentity claims, out string shopId, out long userId)
        {
            shopId = null;
            userId = 0;

            string iis = claims.FindFirst("iss")?.Value;
            if (iis == null)
            {
                _logger.LogWarning("Shopify JWT had no iis value");
                return false;
            }
            if (!Uri.IsWellFormedUriString(iis, UriKind.Absolute))
            {
                _logger.LogWarning($"Shopify JWT iis value {iis} is not a valid url");
                return false;
            }
            var iisUri = new Uri(iis);

            string dest = claims.FindFirst("dest")?.Value;
            if (dest == null)
            {
                _logger.LogWarning("Shopify JWT had no dest value");
                return false;
            }
            if (!Uri.IsWellFormedUriString(dest, UriKind.Absolute))
            {
                _logger.LogWarning($"Shopify JWT iis value {dest} is not a valid url");
                return false;
            }
            var destUri = new Uri(dest);

            if (iisUri.GetLeftPart(UriPartial.Authority) != destUri.GetLeftPart(UriPartial.Authority))
            {
                _logger.LogWarning($"Shopify JWT iis {iis} and dest {dest} didn't match on their left part.");
                return false;
            }

            shopId = destUri.Host;

            string sub = claims.FindFirst(ClaimTypes.NameIdentifier)?.Value //seems asp.net automatically maps 'sub' to another name
                                ?? claims.FindFirst("sub")?.Value; //just in case it changes in a future aspnet version

            if (!long.TryParse(sub, out userId))
            {
                _logger.LogWarning($"Shopify JWT sub {sub} doesn't parse to a numeric user id");
                return false;
            }

            return true;
        }

clement911 avatar Apr 20 '22 05:04 clement911

@clement911 Thank you so much for helping me out mate!

deepakkumar1984 avatar Apr 20 '22 06:04 deepakkumar1984