set-cookie AWS Serverless - node.js

Goal: Set a cookie from aws serverless.
I'm using a custom authentication flow
domain: mydomain.com
current domain: dev.mydomain.com
login api (api gateway): account-api.mydomain.com
Login Lambda
the login function is the actual function invoked
This lambda receives a username & password and creates/returns a JWT & cookie string, I've removed non-pertinent logic
*Right now my response contains extra stuff to help me debug/figure out how to map -- I'll be migrating it out once this is successfully setting the cookie
...
const handler = async event => {
const jwtBody = {
email: event.email,
uuid: current_user_info.uuid.S,
zipcode: current_user_info.zipcode.S,
}
var now = new Date();
var time = now.getTime();
var expireTime = time + (milliToHour*24*10);
now.setTime(expireTime);
var jwt = jsonwebtoken.sign(jwtBody, SMCData.secret, { algorithm: SMCData.alg, expiresIn: '1hr'});
const cookieString = "token="+jwt+";expires=" + now.toUTCString() + ";secure;HttpOnly;"
return {
statusCode: 200,
payload: {
verified: current_user_info.verified.BOOL,
jwt: jwt,
cookie: cookieString
}
}
}
const login = middy(handler).use(cors({
origins:[
"https://dev.mydomain.com",
"https://account-api.mydomain.com",
"https://*.mydomain.com"
],
credentials:true
}))
Current Status - postman
post_body = {
"email": "valid_email#email.com",
"password": "correct_password"
}
response_body = {
"statusCode":200,
"payload":{
"verified":false,
"jwt":"eyJh...KAQ",
"cookie":"token=ey...KAQ;expires=Tue, 12 Nov 2019 22:10:32 GMT;secure;HttpOnly;"
}
}
cookie is also set:
Current Status - chrome
Headers:
post_body = {
"email": "valid_email#email.com",
"password": "correct_password"
}
response_body = {
"statusCode":200,
"payload":{
"verified":false,
"jwt":"eyJh...KAQ",
"cookie":"token=ey...KAQ;expires=Tue, 12 Nov 2019 22:10:32 GMT;secure;HttpOnly;"
}
}
cookie is not set:
API Gateway Configuration
CORS is enabled
*I Know I'm 'supposed' to change the mapping value in the integration response into a mapping template, but I wanted to get things working before I figured out how to make that change.

It helps when you setup cors properly in API Gateway. DOH!

Related

NextAuth refresh token with Azure AD

Since the default valid time for an access token is 1 hour, I am trying to get refresh tokens to work in my application. I have been stuck with this problem for a couple of weeks now, and cannot seem to fix it. I have verified my refreshAccessToken(accessToken) function to work (where accessToken is an object with the expired token, the refresh token and some other stuff).
I pinpointed the issue to the async session() function. While in the async jwt() function, refreshAccessToken gets called, the async session() function still results in an error, because the parameter token is undefined. This results in the error cannot read property accessToken from undefined (since token.accessToken is on the first line). How could I solve this, and somehow make the function async session() wait for the access token to refresh, before sending it to the client, together with all other information (groups, username etc.)?
/**
* All requests to /api/auth/* (signIn, callback, signOut, etc.) will automatically be handled by NextAuth.js.
*/
import NextAuth from "next-auth"
import AzureAD from "next-auth/providers/azure-ad";
async function refreshAccessToken(accessToken) {
try {
const url = "https://login.microsoftonline.com/02cd5db4-6c31-4cb1-881d-2c79631437e8/oauth2/v2.0/token"
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `grant_type=refresh_token`
+ `&client_secret=${process.env.AZURE_AD_CLIENT_SECRET}`
+ `&refresh_token=${accessToken.refreshToken}`
+ `&client_id=${process.env.AZURE_AD_CLIENT_ID}`
}).then(res => res.json())
.then(res => {
return {
...accessToken,
accessToken: res.access_token,
accessTokenExpires: Date.now() + res.expires_in * 1000,
refreshToken: res.refresh_token ?? accessToken.refreshToken, // Fall backto old refresh token
}
})
} catch (error) {
console.log(error)
return {
...accessToken,
error: "RefreshAccessTokenError",
}
}
}
export const authOptions = {
providers: [
AzureAD({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
authorization: {
params: {
scope: "offline_access openid profile email Application.ReadWrite.All Directory.ReadWrite.All " +
"Group.ReadWrite.All GroupMember.ReadWrite.All User.Read User.ReadWrite.All"
}
}
}),
],
callbacks: {
async jwt({token, account, profile}) {
// Persist the OAuth access_token and or the user id to the token right after signin
if (account && profile) {
token.accessToken = account.access_token;
token.accessTokenExpires = account.expires_at * 1000;
token.refreshToken = account.refresh_token;
token.id = profile.oid; // For convenience, the user's OID is called ID.
token.groups = profile.groups;
token.username = profile.preferred_username;
}
if (Date.now() < token.accessTokenExpires) {
return token;
}
return refreshAccessToken(token);
},
async session({session, token}) {
// Send properties to the client, like an access_token and user id from a provider.
session.accessToken = token.accessToken;
session.user.id = token.id;
session.user.groups = token.groups;
session.user.username = token.username;
const splittedName = session.user.name.split(" ");
session.user.firstName = splittedName.length > 0 ? splittedName[0] : null;
session.user.lastName = splittedName.length > 1 ? splittedName[1] : null;
return session;
},
},
pages: {
signIn: '/login',
}
}
export default NextAuth(authOptions)
I tried searching everywhere online, but it is hard to even find people using Azure AD with NextAuth at all (NOT the B2C version, but the B2B/organisations version).
TLDR: Using the refreshToken to get a new accessToken works, but NextAuth does not pass this token to the frontend, and instead throws an error, since in async session(), token is undefined.
In the provided code, I made the mistake of mixing async with promises (credits to #balazsorban44 on GitHub). This means that the return statement in the .then returns the value to where the fetch request was initiated, instead of it returning a value from the function refreshAccessToken() as a whole. I moved away from using promises and only used an async function:
const req = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `grant_type=refresh_token`
+ `&client_secret=${process.env.AZURE_AD_CLIENT_SECRET}`
+ `&refresh_token=${accessToken.refreshToken}`
+ `&client_id=${process.env.AZURE_AD_CLIENT_ID}`
})
const res = await req.json();

How to verify ID token from AAD, and then call user info

I am trying to log in from Azure Active Directory, and do get a token in return. How do I now verify said token and log in?
I am also trying to get access_token and get userinfo, but get an error when adding token to response_type.
My client code is:
export function AzureLogin(){
useEffect(async () => {
const { authorization_endpoint } = await fetchJSON(
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration"
);
const parameters = {
client_id: "my client id",
response_type: "id_token (id_token%20token when trying to get access_token)",
scope: "openid",
nonce: "123",
redirect_uri: window.location.origin + "/login/azure/callback"
}
window.location.href =
authorization_endpoint + "?" + new URLSearchParams(parameters)
}, [])
}
export function AzureCallback(){
useEffect(async () => {
const { id_token } = Object.fromEntries(
new URLSearchParams(window.location.hash.substring(1))
);
await fetch("/api/login/azure", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ id_token }),
})
})
}
And my server code is:
router.post("/azure", async (req, res) => {
const { id_token } = req.body;
res.cookie("id_token", id_token, {signed: true, maxAge: 2 * 60 * 60 * 1000});
const {userinfo_endpoint} = await fetchJSON(
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
);
const userinfo = await fetchJSON(userinfo_endpoint, {
headers: {
Authorization: `Bearer ${id_token}`,
},
})
console.log(userinfo)
const name = userinfo.name;
const id = userinfo.sub;
const email = userinfo.email;
const picture = userinfo.picture;
db stuff
res.sendStatus(200);
})
To answer your question in your comment.
The purpose of the ID-token is to tell the client details about how the user authenticated. It is typically only used to create the local "session" in the client and then thrown away.
The ID-token typically have a very short lifetime (like 5 minutes in some setups) and it is not supposed to be sent to external services/APIs.
The client is not supposed to look inside the access-token and it doesn't even need to validate it. It is the job of the API that later receives the token to validate the signature of the access-token against the public signing key in AzureAD.

How to call a restricted firebase function with a service account key file?

I am trying to setup a restricted firebase function that can be called from another client application that runs outside GCP. So far I failed to setup the client application authentication to get passed the restricted access on the firebase funciton.
Here is what I did and tried:
I created and deployed a simple helloWorld firebase function and verified that the function could be called from the client application with the default public access.
I removed allUsers from the helloWorld permissions on GCP and verified that the function could no longer be called from the client application (I get "403 Forbidden" in the response).
I created a new service account and added it as a member of "Cloud functions invoker" in the permissions panel of helloWorld on the GCP.
I created a new private json key file for this service account.
Then I followed the documentation to setup the client application authentication (see code below).
const fetch = require('node-fetch');
const jwt = require('jsonwebtoken');
async function main(){
// get unix timestamp in seconds
const current_time = Math.floor(Date.now() / 1000)
// get the service account key file
const service_account = require('./service_account.json');
// create the jwt body
const token_body = {
"iss": service_account.client_email,
"scope": "https://www.googleapis.com/auth/cloud-platform",
"aud": "https://oauth2.googleapis.com/token",
"exp": current_time + 3600,
"iat": current_time
}
// sign the token with the private key
const signed_token = jwt.sign(
token_body, service_account.private_key, { algorithm: 'RS256' }
)
// get an access token from the authentication server
const access_token = await fetch(
'https://oauth2.googleapis.com/token',
{
method: 'POST',
body: ''
+ 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer'
+ '&'
+ 'assertion=' + signed_token,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
).then(res => res.json()).then(body => body.access_token)
// call the firebase function with the Authorization header
return fetch(
url_hello_world, { headers: { 'Authorization': 'Bearer ' + access_token } }
).then(res => res.text()).then(console.log)
}
main().catch(console.error)
Unfortunately when I run the previous code I get "401 Unauthorize" with the following header:
www-authenticate: Bearer error="invalid_token" error_description="The access token could not be verified"
After that I tried another approach with the following tutorial (see code below).
const fetch = require('node-fetch');
const util = require('util');
const exec = util.promisify(require("child_process").exec)
async function main(){
// activate a service account with a key file
await exec('gcloud auth activate-service-account --key-file=' + key_file)
// retrieve an access token for the activated service account
const {stdout, stderr} = await exec("gcloud auth print-identity-token")
// get the access token from stdout and remove the new line character at the
// end of the string
const access_token = stdout.slice(0,-1)
// call the firebase function with the Authorization header
const response = await fetch(
url_hello_world,
{ headers: { 'Authorization': 'Bearer ' + access_token } }
)
// print the response
console.log(await response.text())
}
main().catch(console.error)
When I run this code, I get the expected response "Hello World" so the previous code can call the firebase function with the service account permission.
However, the client application that I target cannot rely on the gcloud cli and I am stuck to the point where I tried to understand what does not work in the first version above and what I need to change to make it works.
As you're using a Service Account JSON file, the following code can be helpful.
package.json
{
"name": "sample-call",
"version": "0.0.1",
"dependencies": {
"googleapis": "^62.0.0",
"node-fetch": "^2.6.1"
}
}
index.js
var { google } = require('googleapis')
const fetch = require('node-fetch')
const fs = require('fs');
let privatekey = JSON.parse(fs.readFileSync('key.json'));
let url_hello_world = 'CLOUD_FUNCTION_URL';
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
url_hello_world
)
async function main(){
jwtClient.authorize( async function(err, _token) {
if (err) {
console.log(err)
return err
} else {
const response = await fetch(
url_hello_world,
{
headers: { 'Authorization': 'Bearer ' + _token.id_token }
}
)
console.log(await response.text())
}
})
}
main().catch(console.error)

Client must be authenticated to access this resource. jira rest api

I've created a project using node module passport-atlassian-oauth2 and i get the accessToken successfully. But when I do a request to create an issue I get the error
Client must be authenticated to access this resource.
Below is my code for create issue jira api.
Could you help please?
var bodyData = {
"fields": {
"project": {
"key": "FLUX"
},
"summary": "REST ye merry gentlemen.",
"description": "Creating of an issue using project keys and issue type names using the REST API",
"issuetype": {
"name": "Bug"
}
}
};
var baseUrl = 'https://alamrezoanul.atlassian.net';
var options = {
method: 'POST',
url: `${baseUrl}/rest/api/3/issue`,
data: JSON.stringify(bodyData),
headers: { 'Authorization': 'Bearer ' + jiraTokens.accessToken, 'Content-Type': 'application/json' },
json: true };
axios(options)
.then((response2) => {
console.log("response2.data: ", response2.data);
})
.catch((error) => {
console.log("error: ", error);
})
Hi I encountered the same issue today. You need to fetch the cloud id of https://alamrezoanul.atlassian.net and then instead of doing var baseUrl = 'https://alamrezoanul.atlassian.net'; use var baseUrl = 'https://api.atlassian.com/ex/jira/{cloud id}';.
You can fetch the cloud id by doing a authenticated GET request to https://api.atlassian.com/oauth/token/accessible-resources.
You might want to check the audience in your authorised tokens.
https://developer.atlassian.com/cloud/jira/platform/oauth-2-authorization-code-grants-3lo-for-apps/#implementing-oauth-2-0--3lo-
Setup says:
audience: (required) Set this to api.atlassian.com.
You may need to set to alamrezoanul.atlassian.net
Same issue I have fixed instead username and password use email and api token link to generate token
Below code I am using in my project
public String getEncodedAuth() {
String username = "user#gmail.com";//enter your mail
String auth_header = username + ":" + "<API token>";
String encodedAuth = Base64.getEncoder().encodeToString(auth_header.getBytes());
return encodedAuth;
}
public void getIssue(String issueId) {
Response response = RestAssured.given()
.header("Authorization", "Basic " + getEncodedAuth())
.contentType(ContentType.JSON)
.pathParam("issueIdOrKey", issueId)
.queryParam("fields", "attachment")
.when()
.get("https://user.atlassian.net/rest/agile/1.0/issue/{issueIdOrKey}");
response.prettyPrint();
}

Send mail via Google Apps Gmail using service account domain wide delegation in nodejs

I've been reading tutorials and seeing examples for 2 days already, with no success.
I want to send an email using Google Apps Gmail account in NodeJS environment, however, i get 400 response from Google API:
{[Error: Bad Request]
code: 400,
errors:
[{
domain: 'global',
reason: 'failedPrecondition',
message: 'Bad Request'
}]
}
Here's what I've done so far:
Created a project in Google Cloud Platform
Created a service account
Enabled Domain Wide Delegation for the service account
Downloaded the key for the service account in JSON format
API Manager > Credentials i have created OAuth 2.0 client ID
Enabled Gmail API for the project
In Google Apps Admin console:
In Security > Advanced Settings > Manage API client access i have added the Client ID from step 4 above
I have added all possible scopes for the Client ID
Here's the code that tries to send an email:
const google = require('googleapis');
const googleKey = require('./google-services.json');
const jwtClient = new google.auth.JWT(googleKey.client_email, null, googleKey.private_key, ['https://www.googleapis.com/auth/gmail.send'], null);
jwtClient.authorize((err, tokens) => {
if (err) {
console.err(err);
return;
}
console.log('Google auth success')
var gmail = google.gmail({version: 'v1', auth: jwtClient})
var raw = <build base64 string according to RFC 2822 specification>
var sendMessage = gmail.users.messages.send({
auth: jwtClient,
userId: 'user#domain.com',
message: {
raw: raw
}
}, (err, res) => {
if (err) {
console.error(err);
} else {
console.log(res);
}
});
I can see the Google auth success message and the request is sent with properly initialized token:
headers:
{ Authorization: 'Bearer ya29.CjLjAtVjGNJ8fcBnMSS8AEXAvIm4TbyNTc6g_S99HTxRVmzKpWrAv9ioTI4BsLKXW7uojQ',
'User-Agent': 'google-api-nodejs-client/0.9.8',
host: 'www.googleapis.com',
accept: 'application/json',
'content-type': 'application/json',
'content-length': 2 }
But still, the response is 400
So I was half-step close to the solution, the problem was that while creating const jwtClient = new google.auth.JWT(googleKey.client_email, null, googleKey.private_key, ['https://www.googleapis.com/auth/gmail.send'], null); i did not mention the account to be impersonated.
The correct initialization should be:
const jwtClient = new google.auth.JWT(googleKey.client_email, null, googleKey.private_key, ['https://www.googleapis.com/auth/gmail.send'], 'user#domain.com');
To summarize, the correct steps are:
Created a project in Google Cloud Platform
Created a service account
Enabled Domain Wide Delegation for the service account
Downloaded the key for the service account in JSON format
API Manager > Credentials i have created OAuth 2.0 Client ID
Enabled Gmail API for the project
In Google Apps Admin console:
In Security > Advanced Settings > Manage API client access i have added the Client ID from step 4 above
I have added all possible scopes for the Client ID
This is the code that sends mails:
const google = require('googleapis');
const googleKey = require('./google-services.json');
const jwtClient = new google.auth.JWT(googleKey.client_email, null, googleKey.private_key, ['https://www.googleapis.com/auth/gmail.send'], '<user to impersonate>');
jwtClient.authorize((err, tokens) => {
if (err) {
console.err(err);
return;
}
console.log('Google auth success')
var gmail = google.gmail({version: 'v1'})
var raw = <build base64 string according to RFC 2822 specification>
var sendMessage = gmail.users.messages.send({
auth: jwtClient,
userId: '<user to impersonate>',
resource: {
raw: raw
}
}, (err, res) => {
if (err) {
console.error(err);
} else {
console.log(res);
}
});
Hope that would be helpful for others
Thanks very much #agoldis. Both the summary steps and the code were very helpful.
It helped me pick up on a few things I needed to fix on both the GoogleWorkspace and the ApplicationCode end of the communication path. Below is my c# implementation for anyone who needs it.
private static void Try4()
{
try {
//file included in project with properties: CopyToOutputDirectory = CopyAlways
var credential = GoogleCredential.FromFile("MyPrj-MyServiceAccount-Credentials.json")
.CreateScoped(new[] { GmailService.Scope.GmailSend })
.CreateWithUser("WhoIAmImpersonating#bla.com")
.UnderlyingCredential as ServiceAccountCredential;
var service = new GmailService(new BaseClientService.Initializer() { HttpClientInitializer = credential });
var nl = Environment.NewLine;
string plainText = "From: WhoIAmImpersonating#bla.com"
+ nl + "To: myfriend#gmail.com,"
+ nl + "Subject: This is the Subject"
+ nl + "Content-Type: text/html; charset=us-ascii"
+ nl
+ nl + "This is the message text.";
var newMsg = new Message() { Raw = Base64UrlEncode(plainText) };
service.Users.Messages.Send(newMsg, "WhoIAmImpersonating#bla.com").Execute();
Console.WriteLine("Message Sent OK");
}
catch (Exception ex) {
Console.WriteLine("Message failed");
}
}
private static string Base64UrlEncode(string input)
{
var inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
return Convert.ToBase64String(inputBytes)
.Replace('+', '-')
.Replace('/', '_')
.Replace("=", "" );
}

Resources