No Cookie Header in OAuth Callback

No Cookie Header in Square OAuth Callback

Below is the outline of a python Lambda Handler for the Square authorization callback.
This is the callback endpoint added as the OAuth Redirect URL of the Square application.
The Square Hosted UI sends the authorization code to this lambda and it is responsible for the
token exchange and storing the access / refresh tokens in a back end DB.

the request to the lambda does NOT appear to contain a Cookie header.
Am I looking for the wrong thing? In the wrong way?

Request: {‘version’: ‘2.0’, ‘routeKey’: ‘ANY /square_oauth’, ‘rawPath’: ‘/Leedz_Stage_1/square_oauth’, ‘rawQueryString’: ‘code=sandbox-sq0cgb-BIzgndt7_AjMxfAvIDF6NA&response_type=code&state=60b141cc-a519-4643-95eb-a9289f85faaa’, ‘headers’: {‘accept’: ‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7’, ‘accept-encoding’: ‘gzip, deflate, br’, ‘accept-language’: ‘en-US,en;q=0.9’, ‘content-length’: ‘0’, ‘host’: ‘something.execute-api.us-west-2.amazonaws.com’, ‘referer’: ‘https://www.theleedz.com/’, ‘sec-ch-ua’: ‘“Not_A Brand”;v=“8”, “Chromium”;v=“120”, “Google Chrome”;v=“120”’, ‘sec-ch-ua-mobile’: ‘?0’, ‘sec-ch-ua-platform’: ‘“Windows”’, ‘sec-fetch-dest’: ‘document’, ‘sec-fetch-mode’: ‘navigate’, ‘sec-fetch-site’: ‘cross-site’, ‘sec-fetch-user’: ‘?1’, ‘upgrade-insecure-requests’: ‘1’, ‘user-agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36’, ‘x-amzn-trace-id’: ‘Root=1-the_route_key’, ‘x-forwarded-for’: ‘104.173.202.58’, ‘x-forwarded-port’: ‘443’, ‘x-forwarded-proto’: ‘https’}, ‘queryStringParameters’: {‘code’: ‘sandbox-sq0cgb-BIzgndt7_AjMxfAvIDF6NA’, ‘response_type’: ‘code’, ‘state’: ‘60b141cc-a519-4643-95eb-a9289f85faaa’}, ‘requestContext’: {‘accountId’: ‘the_account_ID’, ‘apiId’: ‘the_app_ID’, ‘domainName’: ‘something.execute-api.us-west-2.amazonaws.com’, ‘domainPrefix’: ‘something’, ‘http’: {‘method’: ‘GET’, ‘path’: ‘/Leedz_Stage_1/square_oauth’, ‘protocol’: ‘HTTP/1.1’, ‘sourceIp’: ‘the.source.IP’, ‘userAgent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36’}, ‘requestId’: ‘Rap9jjxjPHcEPxQ=’, ‘routeKey’: ‘ANY /square_oauth’, ‘stage’: ‘Leedz_Stage_1’, ‘time’: ‘12/Jan/2024:07:55:02 +0000’, ‘timeEpoch’: 1705046102458}, ‘isBase64Encoded’: False}



def lambda_handler(event, context):

# get state parameter
# generated in authorization link sent during sign-up


# COOKIE
# 
# 1/9 not getting the cookie at all
# will throw exception
# checkForCookie(event, state, FALSE)
    
# RESPONSE TYPE
# look for 'code' indicating refresh/access token
if (response_type == 'code'):
	doTokenExchange(table, event, the_user)
	handle_success()


def checkForCookie( event, state ) :

cookie_state = ''
cookie = validateHeader(event, 'cookie', 1)

if cookie:
    c = cookies.SimpleCookie(cookie)
    cookie_state = c['OAuthState'].value
    
    # ERROR
    # cookie state fron web client either NULL or doesn't match param state
    if (not cookie_state) or (state != cookie_state):
        raise ValueError("Authorization failed: invalid auth state")
else:
    logger.error("NO COOKIE RECEIVED")


def validateHeader( event, header, required ):

value = ""

if ('headers' not in event):
    if (required):
        raise ValueError("Http Request error.  No headers found")
else:
    return value


if (header not in event['headers']):
    if required:
        raise ValueError("HTTP Request error.  No '" + header + "' header")
else:
    value = event['headers'][header] 
    
return value

This is expected that there aren’t any cookies in the callbacks. :slightly_smiling_face:

Hi Bryan,

thank you for the reply.

I got this idea from the Square OAuth Serverless Example at connect-api-examples/connect-examples/oauth/python-aws-chalice/README.md at master · square/connect-api-examples · GitHub

This example demonstrates a Python implementation of the Square OAuth flow with all the good practices In the authorize() code below – a cookie string is set – I see now this is just good application-level practice, but not explicitly part of the protocol. Is that right?

set the Auth_State cookie with a random uuid string to protect against cross-site request forgery. Auth_State will expire in 60 seconds once the page is loaded, you can customize the timeout based on your scenario. HttpOnly helps mitigate XSS risks and SameSite helps mitigate CSRF risks.

def authorize():

state = str(uuid.uuid4())
cookie_str = ‘OAuthState={0}; HttpOnly; Max-Age=60; SameSite=Lax’.format(state)

create the authorize url with the state

authorize_url = conduct_authorize_url(state)

render the page

body = ('Click here ’ +
‘to authorize the application -’ + os.environ[‘application_id’])
return Response(
body=html_template.format(body),
status_code=200,
headers={
‘Content-Type’: ‘text/html’,
‘Set-Cookie’: cookie_str
}
)

You are correct in your understanding that setting a cookie with a random UUID string as part of the OAuth authorization process is a good practice at the application level, rather than an explicit requirement of the OAuth protocol itself.

The primary purpose of this practice is to enhance security and mitigate certain types of attacks:

  1. Cross-Site Request Forgery (CSRF): By setting a unique state parameter (the UUID string) in a cookie, you can verify that the authorization request originates from your application and not from a malicious third party. When the OAuth service redirects back to your application, you can check if the state parameter matches the one you set in the cookie, confirming the authenticity of the request.
  2. Cross-Site Scripting (XSS): The HttpOnly flag in the cookie prevents client-side scripts from accessing the cookie data, thus protecting against XSS attacks where an attacker might try to steal session tokens or other sensitive information stored in cookies.
  3. SameSite attribute: This attribute controls whether a cookie is sent with cross-site requests, providing some protection against CSRF attacks. Setting SameSite=Lax allows the cookie to be sent with top-level navigations, which is suitable for the OAuth flow where the user is redirected back to your application.

The Max-Age=60 attribute sets the cookie to expire after 60 seconds, which is a reasonable timeframe for a user to complete the OAuth authorization process. You can adjust this value based on your specific use case and security considerations.

In summary, while these cookie settings are not part of the OAuth protocol itself, they are recommended security measures to protect both your application and your users during the OAuth flow. :slightly_smiling_face:

Thanks Bryan – this is my issue. When I construct the initial authorization URL I create a state = str(uuid.uuid4()) and I store that state in my application-level DB user object. I send the user that link as a param to the url itself – &client_id=…&scope=…&state= state

In my callback authorization lambda I GET the state param, match it up with the UUID I stored in the DB, and authorize the user at the application level. Everything works. But there is no cookie. I can’t control what the Square server sends my callback.

Even if I make a cookie when I create the auth link

cookie_str = ‘OAuthState={0}; HttpOnly; Max-Age=300; SameSite=Lax’.format(state)

I have no way to get that cookie_str sent as a header from Square to me when the user authorizes.

It seems like the cooke_str is just another param like state – and state is doing the CORS work – is that correct?

!!! BTW – real quick I totally appreciate the help and have made tremendous progress. I’m almost there – the sandbox doesn’t allow testing certain things but I’m almost ready to launch and I will be happy to sing you and Square’s praises if this all works.

+Scott
theleedz.com

Yes, the state is doing the CORS work in this case. :slightly_smiling_face:

Hi Bryan, this is another note that is not Square-specific, but deals with the sample code provided by Square which implements the OAuth functions in Python.

https://github.com/square/connect-api-examples/blob/master/connect-examples/oauth/python-aws-chalice/

The Python Chalice Oauth implementation is 2 layers –
1 ) the app client – this is the chalice part

  1. the DB / Oauth protocol layer – in the ‘chalice’ folder.

When the Oauth access / refresh tokens are obtained the expires_at date is returned as a String in the ISO format with the T and the Z.

2024-02-10T08:32:54Z – this is stored as a String in DDB

In oauthDB.py this code is used to retrieve the String and tries to use a <= comparison on it to compare it to a deadline date – also in ISO form.

ex_date = datetime.strftime(datetime.now() + timedelta(22), ‘%Y-%m-%d’)

expiring_oauth_records_response = dynamodb_client.scan(
TableName = oauth_table_name,
FilterExpression = ‘AccessTokenExpiresAt <= :exDate’,
ExpressionAttributeValues = { ‘:exDate’: { ‘S’: expire_cut_date } }
)

This does not work, and wouldn’t work - because it’s a <= on two formatted Strings. Even in the code the ‘S’ indicates to DDB just that. The thing to do of course is just to store the raw int and do the comparison numerically.

I didn’t want to put this on the forum, but it’s another area where the chalice code is bound to cause confusion for future implementors.

Thank you,

+scottgross.works

theleedz.com

Thanks for bringing this to our attention. I’ll be sure to share this with the team. :slightly_smiling_face:

SCENARIO
theleedz.com brokers the payment of a digital asset from Buyer → Seller with the app taking a small fee. In the scenario provided the seller is ‘scott.gross’ and the buyer is ‘theleedz’ – an admin accounts up just like any other user. I have two Square accounts – two white cards one for scott.gross and one for theleedz. I have tested it both ways - with either party being buyer/seller.

SANDBOX
In the sandbox I was able authorize both accounts and generate access/refresh tokens. In the sandbox I can create a payment link and proceed through to the sample payment dialog with the caveat that to proceed further, to test app fee etc., I would need to switch to production.

I switched to production – create_payment_link is broke.

TESTING

SELLER
scott.gross
ACCESS TOKEN:

BUYER
theleedz
ACCESS TOKEN:

AWS

APP: My app is sending the following request – this is taken right from AWS logs:
URL: https://developer.squareup.com/explorer/square/checkout-api/create-payment-link

HEADER
{‘Accept’: ‘application/json’, ‘Square-Version’: ‘2023-12-13’, ‘Authorization’: ‘Bearer ACCESS_TOKEN’, ‘Content-Type’: ‘application/json’}

BODY
{“checkout_options”: {“app_fee_money”: {“currency”: “USD”, “amount”: 18}, “redirect_url”: “https://theleedz.com/hustle.html”, “merchant_support_email”: “[email protected]”, “ask_for_shipping_address”: “false”}, “quick_pay”: {“location_id”: “L40JHVTYW8QZW”, “name”: “scott's airbrush 2/20”, “price_money”: {“currency”: “USD”, “amount”: 200}}, “description”: “[airbrush] scott's airbrush 2/20 (90034)”, “payment_note”: “151882451|airbrush|theleedz”}’

The result is HTTP ERROR 400 Bad Request

WEB CLIENT

https://developer.squareup.com/explorer/square/checkout-api/create-payment-link

curl https://connect.squareup.com/v2/online-checkout/payment-links
-X POST
-H ‘Square-Version: 2024-01-18’
-H ‘Authorization: Bearer ACCESS TOKEN’
-H ‘Content-Type: application/json’
-d ‘{
“quick_pay”: {
“location_id”: “L40JHVTYW8QZW”,
“name”: “Sample Caricature Party Sample”,
“price_money”: {
“amount”: 200,
“currency”: “USD”
}
},
“payment_note”: “62732928|caricatures|scott.gross”,
“description”: “[caricatures] Sample Caricature Party Sample (90034)”,
“checkout_options”: {
“app_fee_money”: {
“amount”: 18,
“currency”: “USD”
},
“redirect_url”: “https://theleedz.com/hustle.html”,
“merchant_support_email”: “[email protected]”,
“ask_for_shipping_address”: false
}
}’

------ RESPONSE ------

cache-control: no-cache
content-length: 119
content-type: application/json
date: Tue, 06 Feb 2024 23:18:06 GMT
square-version: 2024-01-18

{
“errors”: [
{
“category”: “INVALID_REQUEST_ERROR”,
“code”: “INVALID_VALUE”,
“detail”: “Invalid location id: L40JHVTYW8QZW.”
}
]
}

******** IF I replace the seller’s token with the buyer’s token – it works!

curl https://connect.squareup.com/v2/online-checkout/payment-links
-X POST
-H ‘Square-Version: 2024-01-18’
-H ‘Authorization: Bearer ACCESS_TOKEN’
-H ‘Content-Type: application/json’
-d ‘{
“quick_pay”: {
“location_id”: “L40JHVTYW8QZW”,
“name”: “Sample Caricature Party Sample”,
“price_money”: {
“amount”: 200,
“currency”: “USD”
}
},
“payment_note”: “62732928|caricatures|scott.gross”,
“description”: “[caricatures] Sample Caricature Party Sample (90034)”,
“checkout_options”: {
“app_fee_money”: {
“amount”: 18,
“currency”: “USD”
},
“redirect_url”: “https://theleedz.com/hustle.html”,
“merchant_support_email”: “[email protected]”,
“ask_for_shipping_address”: false
}
}’

PRODUCES

{
“payment_link”: {
“id”: “6OROXAQUJYERW2BJ”,
“version”: 1,
“description”: “[caricatures] Sample Caricature Party Sample (90034)”,
“order_id”: “HNUwrZ6MP9snpqzE8FSH8B6QYxCZY”,
“checkout_options”: {
“redirect_url”: “https://theleedz.com/hustle.html”,
“merchant_support_email”: “[email protected]”,
“ask_for_shipping_address”: false,
“app_fee_money”: {
“amount”: 18,
“currency”: “USD”
}
},
“url”: “https://square.link/u/oVcvJNDO”,
“long_url”: “https://checkout.square.site/merchant/MLT52FZYSY9GD/order/HNUwrZ6MP9snpqzE8FSH8B6QYxCZY”,
“created_at”: “2024-02-06T23:19:32Z”,
“payment_note”: “62732928|caricatures|scott.gross”
},
“related_resources”: {
“orders”: [
{
“id”: “HNUwrZ6MP9snpqzE8FSH8B6QYxCZY”,
“location_id”: “L40JHVTYW8QZW”,
“source”: {
“name”: “The Leedz”
},
“line_items”: [
{
“uid”: “EZqSNyEeMVyTOP1NlS4Lb”,
“name”: “Sample Caricature Party Sample”,
“quantity”: “1”,
“item_type”: “ITEM”,
“base_price_money”: {
“amount”: 200,
“currency”: “USD”
},
“variation_total_price_money”: {
“amount”: 200,
“currency”: “USD”
},
“gross_sales_money”: {
“amount”: 200,
“currency”: “USD”
},
“total_tax_money”: {
“amount”: 0,
“currency”: “USD”
},
“total_discount_money”: {
“amount”: 0,
“currency”: “USD”
},
“total_money”: {
“amount”: 200,
“currency”: “USD”
},
“total_service_charge_money”: {
“amount”: 0,
“currency”: “USD”
}
}
],
“fulfillments”: [
{
“uid”: “yr1vvNP6Ml2qJtU06QFZn”,
“type”: “DIGITAL”,
“state”: “PROPOSED”
}
],
“net_amounts”: {
“total_money”: {
“amount”: 200,
“currency”: “USD”
},
“tax_money”: {
“amount”: 0,
“currency”: “USD”
},
“discount_money”: {
“amount”: 0,
“currency”: “USD”
},
“tip_money”: {
“amount”: 0,
“currency”: “USD”
},
“service_charge_money”: {
“amount”: 0,
“currency”: “USD”
}
},
“created_at”: “2024-02-06T23:19:32.662Z”,
“updated_at”: “2024-02-06T23:19:32.662Z”,
“state”: “DRAFT”,
“version”: 1,
“total_money”: {
“amount”: 200,
“currency”: “USD”
},
“total_tax_money”: {
“amount”: 0,
“currency”: “USD”
},
“total_discount_money”: {
“amount”: 0,
“currency”: “USD”
},
“total_tip_money”: {
“amount”: 0,
“currency”: “USD”
},
“total_service_charge_money”: {
“amount”: 0,
“currency”: “USD”
},
“net_amount_due_money”: {
“amount”: 200,
“currency”: “USD”
}
}
]
}
}

ANALYSIS

The Leedz (Main)

L40JHVTYW8QZW

Location ID comes from the application developer console. It is associated with ‘theleedz’ because theleedz is a character in the scenario – but it would have no connection to two random users of the system. The checkout link ONLY works when the access token of the application – matching the LocationID – is encoded into the link.

This is a problem for me. The checkout link must be encoded with the seller’s info – not the buyer’s info. And not the app info. The seller is giving permission to the app to deposit the sale money into their Square account (and subtract the app fee). The buyer is brand new to the system and just paying for a digital asset with a credit card. They are providing all the required ‘authorization’ in the checkout dialog when they authorize payment. Although I store access/refresh tokens for the buyer when they sign up – that is only so they can then sell and collect money. It should be possible to use the system just by coming to the page and seeing a list of checkout links and buying one. There is no way to determine the locationID of a random buyer.

+Scott

theleedz.com

**** The Location ID comes from the Square Developer console. It is associated with ‘theleedz’ – the buyer.

When creating the payment link you’ll need to use the OAuth access token for the sellers account. Also the access token is secret and should never be shared. We recommend recycling the access tokens you include in the post. :slightly_smiling_face:

I’m sorry Bryan I’m really trying hard to understand your answer and solve my problem but I am still confused.

In my scenario there are three parties – the buyer, the seller, and the app. I am trying to create a system where

  1. The seller obtains auth tokens
  2. The seller posts an item for sale
    – the app encodes the seller’s info in the payment link
  3. The buyer clicks the item - goes through payment flow
  4. The app
    – charges the buyer
    – pays the seller
    – pays the app fee to the app

In the Square documentation the app fee seems designed for this use case - there are diagrams showing this use case.

Does the location ID have to match the seller? If so, how can I determine the seller’s Location ID? I tried the Location API – see prev post – but was unable to use it even when the permissions were granted.

I’m lost.

The seller will never need to get an access token. Your application will handle the access token for them. For example your app will create the access token with OAuth for the sellers account. Then the seller will be able to use the application you built to make sales to buyers.

Yes, you’ll use the Locations API to get the sellers location_id once you have the sellers OAuth access token. :slightly_smiling_face:

Yes, I understand about token management and I was just trying to simplify the explanation. All of the tokens / oauth are implemented according to the standard and what sample code is available.

HOWEVER

I am struggling with the Locations API

The oauth grant includes

“client_id”: “sq0idp-fGPn-QZvqEnvGeWHA9sHMw”,

“grant_type”: “authorization_code”,
“scopes”: [
“ORDERS_WRITE”,
“ORDERS_READ”,
“PAYMENTS_WRITE”,
“PAYMENTS_READ”,
“PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS”,
“MERCHANT_PROFILE_READ”
]

But when I take the auth token and feed it to

–developer.squareup.com/explorer/square_2024-01-18/locations-api/retrieve-location

I get

“errors”: [
{
“category”: “AUTHENTICATION_ERROR”,
“code”: “INSUFFICIENT_SCOPES”,
“detail”: “The merchant has not given your application sufficient permissions to do that. The merchant must authorize your application for the following scopes: MERCHANT_PROFILE_READ”
`

When you pass that token in RetrieveTokenStatus what’s the response? :slightly_smiling_face:

This is a progress-report that might shed some light, for anyone else in this position.

In order to build a payment link for a seller, I need the seller’s Location ID. To get it, I need to make an API call, and have the seller’s permissions scopes include MERCHANT_PROFILE_READ

Below in REQUEST is the Oauth request received by Square in the Developer Console. The user (me) is requesting authorization. The RESPONSE indicates success.

I goto Developer Console → Retrieve token status → the RESULT shows that the scopes have not been updated.

Do I have to completely revoke the existing authorization before doing an obtain_token?

Is this a Plan?

  1. delete the existing auth using the existing auth token
  2. do a fresh obtain_token using the client id/secret passed from the hosted UI with full scope
  3. check against retrieve token status
  4. revoke the token again
  5. run the test from scratch with a fresh signup

REQUEST

{
“client_id”: “[redacted]”,
“client_secret”: “[redacted]”,
“code”: “[redacted]”,
“redirect_uri”: “[redacted]”,
“grant_type”: “authorization_code”,
“scopes”: [
“ORDERS_WRITE”,
“ORDERS_READ”,
“PAYMENTS_WRITE”,
“PAYMENTS_READ”,
“PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS”,
“MERCHANT_PROFILE_READ”
]
}


RESPONSE

{
“access_token”: “[redacted]”,
“token_type”: “bearer”,
“expires_at”: “2024-03-08T03:26:44Z”,
“merchant_id”: “[redacted]”,
“refresh_token”: “[redacted]”,
“short_lived”: false
}


Retrieve token status
POST /oauth2/token/status

RESULT

{
“scopes”: [
“ORDERS_READ”,
“ORDERS_WRITE”,
“PAYMENTS_READ”,
“PAYMENTS_WRITE”,
“PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS”
],
“expires_at”: “2024-03-08T03:26:44Z”,
“client_id”: “sq0idp-fGPn-QZvqEnvGeWHA9sHMw”,
“merchant_id”: “516FB1QR33PFH”
}

What’s the link your using to create Authorize the seller? :slightly_smiling_face: