NextAuth Cookie not being set in production - cookies

I have a React + Nextjs application which uses NextAuth. I'm trying to set a refresh token in a cookie which works fine on localhost but doesn't work when deployed on to production (HTTPS). The function setting the cookie is shown below. Is there something that I've missed?
From [...nextauth].ts:
const setRefreshTokenCookie = (
cookies: Cookies,
refresh_token: string
): Cookies => {
const date = new Date();
const time = date.getTime();
const expireTime = time + 24 * 60 * 60 * 1000 * 90; // 90 days
date.setTime(expireTime);
const currentCookies = cookies.set(
`${useSecureCookies ? "__Host-" : ""}test.refresh-token`,
refresh_token,
{
sameSite: "strict",
overwrite: true,
expires: date,
httpOnly: useSecureCookies,
}
);
return currentCookies ;
};
Notes:
NextAuth's useSecureCookie
localhost cookies:
test.csrf-token
test.refresh-token
test.callback-url
test.session-token
production cookies:
__Host-test.csrf-token
__Secure-test.callback-url
__Secure-test.session-token

Related

document.cookie doesn't see embedded expression in template literal

I'm implementing logic for cookie consent with Next.js and Google Analytics 4 (gtag.js). I've used this example as basis. The logic is simple: if user accepted cookies - load script, if not - check if cookies were already present (e.g. user changed their mind) - disable GA cookies and delete current ones, if weren't present (first time consent) - just disable GA cookies.
GA4 sets 3 cookies, one of which is -ga-(container-id). This ID should be stored as env var. To delete cookie I set Max-Age param to zero.
Code:
const router = useRouter()
useEffect(() => {
if (areCookiesAccepted) {
console.log(1)
const handleRouteChange = url => {
gtag.pageview(url)
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
} else {
if (
document.cookie.split(';').some(item => item.trim().startsWith('_ga=')) ||
document.cookie.split(';').some(item => item.trim().startsWith(`_ga_${process.env.NEXT_PUBLIC_GA_ID}=`)) ||
document.cookie.split(';').some(item => item.trim().startsWith('_gid='))
) {
console.log(2.1)
window.gtag('consent', 'update', {'ad_storage': 'denied', 'analytics_storage': 'denied'})
document.cookie = '_ga=; Max-Age=0;'
document.cookie = `_ga_${process.env.NEXT_PUBLIC_GA_ID}=; Max-Age=0;`
document.cookie = '_gid=; Max-Age=0;'
} else {
console.log(2.2)
window.gtag('consent', 'default', {'ad_storage': 'denied', 'analytics_storage': 'denied'})
}
}
}, [router.events, areCookiesAccepted])
The problem is that document.cookie can't see embedded env var in template literal and cookie is not being deleted, but when I expose bare ID without env var - everything works okay. I double checked - no typos in env.local file. Does anybody knows what's wrong here?
So far I ended up with hack. So this doesn't work:
document.cookie = `_ga_${process.env.NEXT_PUBLIC_GA_ID}=; Max-Age=0;`
But this works:
const cookiePair = document.cookie.split('; ').find(row => row.startsWith('_ga_'))
const cookieName = cookiePair.substring(0, cookiePair.indexOf('='))
document.cookie = `${cookieName}=; Max-Age=0;`
Cookie is being deleted successfully. Still, I'm not fully satisfied with this approach and would like to know why initial problem happens.

Connecting to Google API with Service Account and OAuth

I'm using an environment that doesn't have native support for a GCP client library. So I'm trying to figure out how to authenticate directly using manually crafted JWT token.
I've adapted the tasks from here Using nodeJS test environment, with jwa to implement the algorithm.
https://developers.google.com/identity/protocols/OAuth2ServiceAccount
The private key is taken from a JSON version of the service account file.
When the test runs, it catches a very basic 400 error, that just says "invalid request". I'm not sure how to troubleshoot it.
Could someone please help identify what I'm doing wrong?
var assert = require('assert');
const jwa = require('jwa');
const request = require('request-promise');
const pk = require('../auth/tradestate-2-tw').private_key;
const authEndpoint = 'https://www.googleapis.com/oauth2/v4/token';
describe('Connecting to Google API', function() {
it('should be able to get an auth token for Google Access', async () => {
assert(pk && pk.length, 'PK exists');
const header = { alg: "RS256", typ: "JWT" };
const body = {
"iss":"salesforce-treasury-wine#tradestate-2.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/devstorage.readonly",
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp": new Date().getTime() + 3600 * 1000,
"iat": new Date().getTime()
};
console.log(JSON.stringify(body, null, 2));
const encodedHeader = Buffer.from(JSON.toString(header)).toString('base64')
const encodedBody = Buffer.from(JSON.toString(body)).toString('base64');
const cryptoString = `${encodedHeader}.${encodedBody}`;
const algo = jwa('RS256');
const signature = algo.sign(cryptoString, pk);
const jwt = `${encodedHeader}.${encodedBody}.${signature}`;
console.log('jwt', jwt);
const headers = {'Content-Type': 'application/x-www-form-urlencoded'};
const form = {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt
};
try {
const result = await request.post({url: authEndpoint, form, headers});
assert(result, 'Reached result');
console.log('Got result', JSON.stringify(result, null, 2));
} catch (err) {
console.log(JSON.stringify(err, null, 2));
throw (err);
}
});
});
Use JSON.stringify instead of JSON.toString. From the link in your question:
{"alg":"RS256","typ":"JWT"}
The Base64url representation of this is as follows:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
When using JSON.toString() and then base64 encoding, you would get W29iamVjdCBKU09OXQ== which explains the 400 for an invalid request as it can't decrypt anything you're sending it.

Identity Server 3 Token Request from POSTMAN Http Tool

Using POSTMAN, I'm struggling to to retrieve my Identity Server 3 token.
Error code is : 400 Bad Request
Here are the details:
POST /identity/connect/token HTTP/1.1
Host: localhost:44358
Content-Type: application;x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: 57fc7aef-0006-81b2-8bf8-8d46b77d21d1
username=MYUSER-ID&password=MY-PASSWORD&grant_type=password&client_id=rzrwebguiangulajsclient&client_secret=myclientsecret&redirect_uri=https://localhost:44331/callback
I've done something similar with a simple Visual Studio 2015 WebApi project, where the end point was \token.
Any guidance/advice is appreciated...
regards,
Bob
The minimum required for a Resource Owner OAuth request is the following (line breaks added for readability):
POST /connect/token
Header
Content-Type: application/x-www-form-urlencoded
Body
username=MYUSER-ID
&password=MY-PASSWORD
&grant_type=password
&client_id=rzrwebguiangulajsclient
&client_secret=myclientsecret
&scope=api
Off the bat you are not requesting a scope in your request. Otherwise there is most probably something wrong in the configuration of your client within Identity Server.
Your best bet would be to enable logging and look at what comes back when this request errors.
Update: also, please don't use the ROPC grant type
I'm happy to say that we got Postman to work.
It turns out I was so close to getting Postman to work with Identity Server 3 Authorization.
The final piece to the solution was setting the Postman client Flow to Flow = Flows.ClientCredentials (see the postmantestclient client definition below):
using System.Collections.Generic;
using IdentityServer3.Core.Models;
namespace MyWebApi.MyIdentityServer.Config
{
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = MyConstants.MyIdentityServer.MyWebGuiClientId,
ClientName = "My Web Gui Client",
Flow = Flows.Implicit,
AllowAccessToAllScopes = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 300, //5 minutes
RequireConsent = false,
// redirect = URI of the Angular application
RedirectUris = new List<string>
{
MyConstants.MyIdentityServer.MyWebGuiUri + "callback.html",
// for silent refresh
MyConstants.MyIdentityServer.MyWebGuiUri + "silentrefreshframe.html"
},
PostLogoutRedirectUris = new List<string>()
{
MyConstants.MyIdentityServer.MyWebGuiUri + "index.html"
}
},
new Client
{
ClientId = MyConstants.MyIdentityServer.SwaggerClientId,
ClientName = "Swagger Client",
Flow = Flows.Implicit,
AllowAccessToAllScopes = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 300,
RequireConsent = false,
// redirect = URI of the Angular application
RedirectUris = new List<string>
{
"https://localhost:44358/swagger/ui/o2c-html"
}
},
new Client
{
ClientId = "postmantestclient",
ClientName = "Postman http test client",
Flow = Flows.ClientCredentials,
AllowAccessToAllScopes = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 300, //5 minutes
RequireConsent = false,
ClientSecrets = new List<Secret>
{
new Secret("PostmanSecret".Sha256())
},
RedirectUris = new List<string>()
{
"https://www.getpostman.com/oauth2/callback"
}
}
};
}
}
}

Using AWS Gateway API, can I access the cookies?

Using a HTTP Proxy Integration I want to access the cookies and add one to the json response. Is that possible?
To access cookies sent by the client in your backend you'll have to setup a mapping from the method request header to your integration request header.
These instructions assume you've already setup a simple method in API Gateway.
Access cookies in your backend
Under Method Request, create an HTTP Request Header with the name of "Cookie"
Under Integration Request, create an HTTP header with name "Cookie" and "Mapped from" value of method.request.header.Cookie.
You'll also likely need to setup CORS for this method. See: http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html
Deploy your API and make a request to your API Gateway endpoint with your browser/client. You should see requests coming in to your HTTP backend with the Cookie header value sent from the browser.
Add cookie to response
You can setup a Set-Cookie response header in an analogous fashion for the the integration response/method response side of the method configuration.
Under Method Response, create a Response header with name Set-Cookie
Under Integration Response setup a Header Mapping with Response header Set-Cookie and Mapping value integration.response.header.Set-Cookie
Please note that at this time, API Gateway supports setting just a single Set-Cookie response header. If your backend attempts to set multiple Set-Cookie headers, only the last one will be set. See this forum post for more details: https://forums.aws.amazon.com/thread.jspa?messageID=701434
If you check the "Use Lambda Proxy integration" option in your API Gateway method, the request headers will be passed to your Lambda function via the event variable. API Gateway will also expect a different response from your callback function. This response format can be use to dictate a Set-Cookie header. e.g.:
callback(null, {
statusCode: 200,
headers: {'Set-Cookie': 'key=val'},
body: 'Some response'
})`
This approach has the advantage of not requiring any Method Request or Method Response tweaks.
Here's a sample Lambda function using this logic to rotate a cookie value after each request.
exports.handler = (event, context, callback) => {
var cookies = getCookiesFromHeader(event.headers);
var old_cookie = cookies.flavor;
var new_cookie = pickCookieFlavor(old_cookie);
return callback(null, {
statusCode: 200,
headers: {
'Set-Cookie': setCookieString('flavor', new_cookie),
'Content-Type': 'text/plain'
},
body: 'Your cookie flavor was ' + old_cookie + '. Your new flavor is ' + new_cookie + '.'
});
};
/**
* Rotate the cookie flavor
*/
function pickCookieFlavor(cookie) {
switch (cookie) {
case 'peanut':
return 'chocolate';
case 'chocolate':
return 'raisin and oat';
default:
return 'peanut';
}
}
/**
* Receives an array of headers and extract the value from the cookie header
* #param {String} errors List of errors
* #return {Object}
*/
function getCookiesFromHeader(headers) {
if (headers === null || headers === undefined || headers.Cookie === undefined) {
return {};
}
// Split a cookie string in an array (Originally found http://stackoverflow.com/a/3409200/1427439)
var list = {},
rc = headers.Cookie;
rc && rc.split(';').forEach(function( cookie ) {
var parts = cookie.split('=');
var key = parts.shift().trim()
var value = decodeURI(parts.join('='));
if (key != '') {
list[key] = value
}
});
return list;
};
/**
* Build a string appropriate for a `Set-Cookie` header.
* #param {string} key Key-name for the cookie.
* #param {string} value Value to assign to the cookie.
* #param {object} options Optional parameter that can be use to define additional option for the cookie.
* ```
* {
* secure: boolean // Watever to output the secure flag. Defaults to true.
* httpOnly: boolean // Watever to ouput the HttpOnly flag. Defaults to true.
* domain: string // Domain to which the limit the cookie. Default to not being outputted.
* path: string // Path to which to limit the cookie. Defaults to '/'
* expires: UTC string or Date // When this cookie should expire. Default to not being outputted.
* maxAge: integer // Max age of the cookie in seconds. For compatibility with IE, this will be converted to a
* `expires` flag. If both the expires and maxAge flags are set, maxAge will be ignores. Default to not being
* outputted.
* }
* ```
* #return string
*/
function setCookieString(key, value, options) {
var defaults = {
secure: true,
httpOnly: true,
domain: false,
path: '/',
expires: false,
maxAge: false
}
if (typeof options == 'object') {
options = Object.assign({}, defaults, options);
} else {
options = defaults;
}
var cookie = key + '=' + value;
if (options.domain) {
cookie = cookie + '; domain=' + options.domain;
}
if (options.path) {
cookie = cookie + '; path=' + options.path;
}
if (!options.expires && options.maxAge) {
options.expires = new Date(new Date().getTime() + parseInt(options.maxAge) * 1000); // JS operate in Milli-seconds
}
if (typeof options.expires == "object" && typeof options.expires.toUTCString) {
options.expires = options.expires.toUTCString();
}
if (options.expires) {
cookie = cookie + '; expires=' + options.expires.toString();
}
if (options.secure) {
cookie = cookie + '; Secure';
}
if (options.httpOnly) {
cookie = cookie + '; HttpOnly';
}
return cookie;
}

Check expiration times jQuery cookie

I have set a cookie using the jQuery cookie plugin. I've set the expiration on the cookie to one hour, so after that time the cookie is deleted. I want to display the remaining time left until the cookie expires to the user by retrieving this info from the cookie itself. is this possible using the jQuery cookies plugin? If not is there an eloquent way to achieve this?
I've set the expiration in this way:
jQuery.cookie('Cookie', timedCookie, { expires: new Date(+new Date() + (60 * 60 * 1000)) });
It's impossible to get a cookie's expiration using JavaScript. The only way to do this would be to store the expiration date somehow, such as in a javascript variable or in another cookie or local storage.
Here's an example:
var MINUTES = 1000 * 60;
var expireTime = new Date(+new Date + (60 * MINUTES)); // store the expiration time
jQuery.cookie('Cookie', timedCookie, { expires: expireTime });
var updateMessage = function(msg){
document.getElementById('time-left').innerHTML = msg;
};
var i = setInterval(function(){ // calculate time difference every ~1 min
var timeLeft = expireTime - new Date;
if(timeLeft <= 0){
updateMessage('Your session has expired.');
clearInterval(i);
} else {
updateMessage('You have ' + (timeLeft / MINUTES | 0) + ' minute(s) left in your cookied session.');
}
}, MINUTES);