Unable to use custom authorizer in API Gateway - node.js

I have a couple of days trying to secure my API Gateway using custom authorizers with the auth0 service. I have my lambda which validates my bearer token, the Lambda does work if I invoke it inside the AWS console and when I create a custom authorizer I can successfully tested with a Bearer token.
When I try to attach the authorizer to my API Gateway methods and test the request with postman and the token provided by auth0 it always returns a 401 status code. I read my logs in CloudWatch and the authorization Lambda it's never triggered whenever I make the HTTP request. I am following this tutorial:
https://auth0.com/docs/integrations/aws-api-gateway/custom-authorizers/
And this is my Authorization lambda code:
Handler:
'use strict';
let jwtManager = require("./jwt_manager");
module.exports.authenticate = (event, context, callback) => {
jwtManager.validate(event, function (error, data) {
if (error) {
if (!error) {
context.fail("Unhandled error");
}
context.fail(error);
}
else {
console.log("SUCCEED");
context.succeed(data);
}
});
};
And this is the jwtManager:
"use strict";
require("dotenv").config({ silent: true });
let jwksClient = require("jwks-rsa");
let jwt = require("jsonwebtoken");
module.exports.validate = function(params, callback) {
var token = validateParams(params);
var client = jwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: process.env.JWKS_URI
});
var decodedJwt = jwt.decode(token, { complete: true });
var kid = decodedJwt.header.kid;
client.getSigningKey(kid, function(error, data) {
if (error) {
console.log(error);
callback(error);
} else {
var signingKey = data.publicKey || data.rsaPublicKey;
jwt.verify(
token,
signingKey,
{ audience: process.env.AUDIENCE, issuer: process.env.ISSUER },
function(error, decoded) {
if (error) {
console.log("ERROR");
console.log(error);
callback(error);
}
else {
console.log(decoded);
var response = {
principalId: decoded.sub,
policyDocument: getPolicyDocument("Allow", params.methodArn),
context: {
scope: decoded.scope
}
}
console.log(response);
console.log(response.policyDocument);
callback(null, response);
}
}
);
}
});
};
function validateParams(params) {
var token;
if (!params.type || params.type !== "TOKEN") {
throw new Error("Expected 'event.type' parameter to have value TOKEN");
}
var tokenString = params.authorizationToken;
if (!tokenString) {
throw new Error("Expected 'event.authorizationToken' parameter to be set");
}
var match = tokenString.match(/^Bearer (.*)$/);
if (!match || match.length < 2) {
throw new Error(
"Invalid Authorization token - '" +
tokenString +
"' does not match 'Bearer .*'"
);
}
return match[1];
}
function getPolicyDocument(effect, resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17'; // default version
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = [ 'execute-api:Invoke', 'lambda:Invoke'] ; // default action
statementOne.Effect = effect;
statementOne.Resource = resource.split('/')[0] + '/*';
policyDocument.Statement[0] = statementOne;
return policyDocument;
}
Thanks in advance!

I would like to describe how I resolved this issue.
First thing, the custom authorizer always need bearer token in authorizationToken field but from while invoking API Gateway from Postman or any other client you can send the 'Bearer Token' in authorization header, as this is an industry standard, AWS has supported it.
The trick here is in 'Token Source' while configuring the 'custom authorizer'. I have attached an image here where you can configure that 'Token Source' this field describes that the input to custom authorizer is from 'Authorization Header'.
This way, you can still send the token in 'Authorzation' header from postman, and API Gateway would copy it from 'Authorization' header and copy it to 'authorizationToken' input field while invoking custom authorizer lambda.
Hope it's clear. Let me know if you need more details.

When you test an API Gateway with a custom authorizer attached but the auth lambda function never triggered, it is likely due to unsuccessful validation in token header name/ token pattern validation.
I am able to reproduce your issue.
The authorizer can only be triggered IF I change the header name from "Authorization" to "AuthorizationToken" in POSTMAN.
check the token header name I made the authorizer works
I think it is likely a new bug in AWS portal as I noticed they have changed the UI to configure API Gateway Authorizers not long ago.
It is very strange a HTTP request has to send bearer token in a header with name "AuthorizationToken". If your AWS plan allows you to access their technical support, you should alert them about this issue.

In my case, the same error (lambda not triggered, authorizer failing) was due to the fact that I didn't deployed the API yet. I may be wrong, but it seems that for testing an Authorizer, your API has to be deployed at least once.
So, I deployed the API, and the authorizer test began to work.

It isn't triggered this can be due to one of two reasons:
the APIGW authorizer response is cached
your configuration expects the token somewhere else.
In the case of the cache, you'll have to wait until it expires or use a different token. For the point of testing, you could remove the cache. In the latter cases, APIGW authorizers automatically reject requests with missing token when the request does not contain the token in the expected location. In those cases your authorizer is not even used.
You can see in this example the authorizer configuration looks at the Authorization header in the identity sources.
In the case you don't specify the authorization header, then the request is automatically rejected.
The other important part of the request is the Authorizer Type. Your code is validating the event.type to be TOKEN. But TOKEN is the legacy authorizer type. The current best practice is to use REQUEST. This exposes the whole request to your authorizer so that you can directly use the request.headers.Authorization header correctly.
It still isn't obvious the best way to handle this so I generally recommend something like this apigw authorizer library and then combining the parsing that library gives you with handling of the request. An example of how to handle the request can be seen here in an apigw authorizer.

Related

Get token for firebase admin authentication

this is my function, for firebase function:
export const hello = onRequest({ cors }, async (request, response) => {
const token = request.headers.authorization?.split('Bearer ')[1]
if (token) {
const tokenData = await getAuth().verifyIdToken(token, true)
response.send({
status: 'success',
data: tokenData.email
})
} else {
response.status(401).send('Unauthorized')
}
})
I don't like how I'm getting token here, but that's the only way I found:
request.headers.authorization?.split('Bearer ')[1]
is there any better way? Let's say, maybe admin itself has some built in method?
The verifyIdToken() requires only the JWT token as first parameter. The 'Bearer ' just indicates that the request uses Bearer token system. If you just add the token in your API request you won't have to parse the string to get the token part. The Admin SDK does not have any built-in function for that.
Alternatively, you can use onCall() instead of onRequest() that'll handle the authentication part and you can read user's information from context.auth object.

Auth0 "service not found" error

I'm attempting to use Auth0 to issue JWT tokens for accessing my API (so that Auth0 handles all the OAuth and security concerns, etc., and my API just needs to check the token). When I try to test the Authorization Code flow for clients to receive an access token (using Node + Express), the following happens:
The authorization code request works fine, and the client is redirected back to my redirect_uri with the code appended to the query. All good.
The token request then always fails. If I include the audience parameter, the request returns an access_denied error with the following details: Service not found: {the audience parameter}, regardless of what value I set for the audience parameter.
If I don't include the audience parameter, I get a server_error with the message Service not found: https://oauth.auth0.com/userinfo.
I've checked every Auth0 setting and read every documentation page thoroughly, and so far nothing has worked. I've also tested the Authorization Code flow in Auth0's API debugger, and it worked fine. My test follows exactly the same parameters, and yet still receives an error requesting the token. I'm testing on localhost. The client credentials and implicit flows are working fine.
Here is a test endpoint I created which retrieves the authorization code from Auth0:
const qs = require('querystring');
const getCode = (req, res) => {
const params = {
audience, // the value of the API Audience setting for the client
client_id, // the client ID
redirect_uri, // the redirect_uri, which is also listed in the Allowed Callback URLs field
response_type: `code`,
scope: `offline_access open` // ask to return ID token and refresh token,
state: `12345`,
};
const authDomain = `mydomain.auth0.com/oauth`;
res.redirect(`${authDomain}/oauth/authorize?${qs.stringify(params)}`);
};
The redirect_uri then redirects to the following endpoint, where I make the request for the access token:
const https = require('https');
const callback = (req, res) => {
const body = {
client_id,
client_secret,
code: req.query.code,
grant_type: `authorization_code`,
redirect_uri, // same value as provided during the code request
};
const opts = {
headers: { 'Content-Type': `application/json` },
hostname: `mydomain.auth0.com`,
method: `POST`,
path: `/oauth/token`,
};
const request = https.request(opts, response => {
let data = ``;
response.on(`data`, chunk => { data += chunk; });
response.on(`error`, res.send(err.message));
response.on(`end`, () => res.json(JSON.parse(data))); // this executes, but displays the error returned from Auth0
});
request.on(`error`, err => res.send(err.message));
request.end(JSON.stringify(body), `utf8`);
};
Any suggestions as to what I might be doing wrong?
The issue was that I was calling the incorrect URL at Auth0. I mistakenly thought that both the authorization and token endpoints began with /oauth, when in fact the authorization endpoint is just /authorize, while the token endpoint is /oauth/authorize. Correcting the URLs in my code fixed the problem.
My solution was the identifier of the api was not found. If it is not exact it won't find it. I had an extra backslash on my 'audience' where the identifier didnt have one. pretty easy mistake but the error is not very clear in Auth0.
In my case, I was using auth0 react hooks. So the example code looked like this:
const getUserMetadata = async () => {
const domain = process.env.REACT_APP_AUTH0_DOMAIN
try {
const accessToken = await getAccessTokenSilently({
audience: `https://${domain}/api/v2/`,
scope: 'read:current_user',
})
console.log('accessToken', accessToken)
localStorage.setItem('access_token', accessToken)
setUserAuthenticated(true)
} catch (e) {
console.log('error in getting access token', e.message)
}
}
My solution to this was using by default Auth0 Audience value in audience field
const getUserMetadata = async () => {
const auth0audience = process.env.REACT_APP_AUTH0_AUDIENCE
try {
const accessToken = await getAccessTokenSilently({
audience: auth0audience,
scope: 'read:current_user',
})
console.log('accessToken', accessToken)
localStorage.setItem('access_token', accessToken)
setUserAuthenticated(true)
} catch (e) {
console.log('error in getting access token', e.message)
}
}
Because its stated in auth0 docs of configuring custom domains that, you need to use by default API audience
Source - https://auth0.com/docs/brand-and-customize/custom-domains/configure-features-to-use-custom-domains

Authenticate with Azure AD with Node

I have a Native Client Application setup in my Azure Active Directory environment. I am trying to write a Node app for Utility purposes to interact with the Azure Management APIs. My challenge is just authenticating my app. At this time, I have:
let azure = {
clientId: '[only-for-my-eyes]',
key: '[only-for-my-eyes]',
tenantDomain: 'mydomain.onmicrosoft.com',
tenantId: '[only-for-my-eyes]'
};
let authenticationRequest = {
url: `https://login.microsoftonline.com/${azure.tenantDomain}/oauth2/v2.0/authorize`,
headers: {
'Content-Type':'application/x-www-form-urlencoded'
},
formData: {
response_type: 'code',
response_mode: 'form_post',
grant_type:'client_credentials',
resource: 'https://management.azure.com',
client_id: azure.clientId,
client_secret: azure.key
}
};
request.post(authenticationRequest, function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log(response.statusCode);
console.log(response.statusMessage);
}
});
When the above runs, the 200 status code block is executed. But, it just prints out a bunch of HTML. If I'm looking at it correctly, it looks like the HTML of the login screen. I'm trying to get an access token that I can pass to the management APIs.
What am I missing?
Why not just use ARMClient? All the nasty token business is taken care of.
From https://www.npmjs.com/package/armclient:
Initialization:
// ES5
var ArmClient = require('armclient');
var client = ArmClient({
subscriptionId: '111111-2222-3333333',
auth: ArmClient.clientCredentials({
tenantId: '444444-555555-666666666',
clientId: '777777-888888-999999999',
clientSecret: 'aaaabbbbbccccc' // or servicePrincipalPassword
})
});
Get resources in your subscription:
client.get('https://management.azure.com/subscriptions/111-222-333-444/resourceGroups/lab/providers/Microsoft.Automation/automationAccounts', { 'api-version': '2015-10-31' })
.then((res) => {
console.log(res.body);
console.log(res.headers);
})
.catch((err) => {
console.log(err);
});
I believe that particular endpoint is intended for a GET with those given parameters, not a POST. I suspect what you're seeing is probably just the generic error message:
Sorry, but we’re having trouble signing you in.
We received a bad request.
What you are trying to do is to call the authorization page with a POST request. You don't have to send a POST (or GET) request here, you must redirect your user to that authorization URL.
In addition, you must have a redirect URI (I don't see it in your azure object). This redirect URI is a callback to your application. For the rest of my answer, let say it is stored in azure.redirectUri
let url = 'https://login.microsoftonline.com/${azure.tenantDomain}/oauth2/v2.0/authorize?response_type=code&response_mode=form_post&client_id={azureclient_id}&resource=https%3A%2F%2Fmanagement.azure.com&redirect_uri={azure.redirectUri}'
response.writeHead(302, {
'Location': url
});
response.end();
The user will be redirected to the authorization page and will have to accept (or deny) your application request. Then the user is redirected back to your Node.js application (azure.redirectUri).
As your response_mode is form_post, if the user accepted your application request, you will receive the authorization code in the body parameters.
With that code your application will be able to get an access token by calling the token endpoint.

Keep getting 'wrong code' with Deezer oauth and Parse.com via cloud httpRequest (code is OK via PostMan)

I'm setting up the server side of my authentication flow with Deezer. I get an authentication code client side, and then, server side I try to convert it into a token (http://developers.deezer.com/api/oauth).
I have no issue client side, the issue is server side (Parse.com via Cloud code).
I'm doing a request like that:
Parse.Cloud.httpRequest({
method: 'GET',
url: 'https://connect.deezer.com/oauth/access_token',
params: {
app_id: DEEZER_APP_ID,
secret: DEEZER_APP_SECRET,
code: request.params.code
}
}).then(function (httpResponse) {
response.error(httpResponse.text);
});
note: I'm not looking for the error callback because Deezer always respond a 200 (...).
I keep getting a 'wrong code' response by deezer api. When I use the exact same code with Postman (I'm trying the code on Postman AFTER the request on Parse, the code is only valid once), I successfully retrieve the access token.
I'm now using an other nodejs backend (not on Parse) to act like a proxy for this request:
var options = {
url: 'https://connect.deezer.com/oauth/access_token',
qs: {
app_id: req.query.app_id,
secret: req.query.secret,
code: req.query.code
}
};
request.get(options, function (error, response, body) {
res.respond(body);
});
And it works perfectly. But I really don't want to maintains 2 backends for 1 application.
Am I doing something wrong ? Does Parse.com via httpRequest manipulate the query in a special way that deezer api does not understand (but my other backend has no problems with it) ?
UPDATE:
As asked in the comment I'm getting the code this way (Cordova mobile app):
var redirect_uri = "http://myredirecturi.com/callback";
var scope = "basic_access,email,offline_access";
var app_id = DEEZER_APP_ID;
var ref = window.open('https://connect.deezer.com/oauth/auth.php?app_id=' + app_id + '&redirect_uri=' + encodeURIComponent(redirect_uri) + '&response_type=code&scope=' + encodeURIComponent(scope), '_blank', 'location=no');
When the callback is triggered:
var code = (event.url).split("code=")[1];
Parse.Cloud.run('deezerAuth', {
code: code
}).then(function (result) {
});

Authentication on Server side routes in Meteor

What is the best way (most secure and easiest) to authenticate a user for a server side route?
Software/Versions
I'm using the latest Iron Router 1.* and Meteor 1.* and to begin, I'm just using accounts-password.
Reference code
I have a simple server side route that renders a pdf to the screen:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
As an example, I'd like to do something close to what this SO answer suggested:
On the server:
var Secrets = new Meteor.Collection("secrets");
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
And then in client code:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
But even if I somehow got this method working, I'd still be vulnerable to users just putting in a URL like '/pdf-server' unless the route itself somehow checked the Secrets collection right?
In the Route, I could get the request, and somehow get the header information?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
And from the client pass a token over the HTTP header, and then in the route check if the token is good from the Collection?
In addition to using url tokens as the other answer you could also use cookies:
Add in some packages that allow you to set cookies and read them server side:
meteor add mrt:cookies thepumpinglemma:cookies
Then you could have something that syncs the cookies up with your login status
Client Side
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
Server Side
On the server side you just need to check this cookie is valid (with iron router)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
I think I have a secure and easy solution for doing this from within IronRouter.route(). The request must be made with a valid user ID and auth token in the header. I call this function from within Router.route(), which then gives me access to this.user, or responds with a 401 if the authentication fails:
// Verify the request is being made by an actively logged in user
// #context: IronRouter.Router.route()
authenticate = ->
// Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
// Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
// Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
// Attach the user to the context so they can be accessed at this.user within route
this.user = user
// Respond to an HTTP request
// #context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
And something like this from the client:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
This code was heavily inspired by RestStop and RestStop2. It's part of a meteor package for writing REST APIs in Meteor 0.9.0+ (built on top of Iron Router). You can check out the complete source code here:
https://github.com/krose72205/meteor-restivus
Because server-side routes act as simple REST endpoints, they don't have access to user authentication data (e.g. they can't call Meteor.user()). Therefore you need to devise an alternative authentication scheme. The most straightforward way to accomplish this is with some form of key exchange as discussed here and here.
Example implementation:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
secret
{{/with}}
</template>
Notes
Make /secret only accessible via HTTPS.
While it's very likely that the user requesting /secret is currently connected, there is no guarantee that she is. The user could have logged in, copied her key, closed the tab, and initiated the request sometime later.
This is a simple means of user authentication. I would explore more sophisticated mechanisms (see the links above) if the server-route reveals high-value data (SSNs, credit cards, etc.).
See this question for more details on sending static content from the server.
I truly believe using HTTP headers are the best solution to this problem because they're simple and don't require messing about with cookies or developing a new authentication scheme.
I loved #kahmali's answer, so I wrote it to work with WebApp and a simple XMLHttpRequest. This has been tested on Meteor 1.6.
Client
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
Server
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from #kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
I hope this helps someone!
Since Meteor doesn't use session cookies, client must explicitly include some sort of user identification when making a HTTP request to a server route.
The easiest way to do it is to pass userId in the query string of the URL. Obviously, you also need to add a security token that will prove that the user is really who the claim they are. Obtaining this token can be done via a Meteor method.
Meteor by itself doesn't provide such mechanism, so you need some custom implementation. I wrote a Meteor package called mhagmajer:server-route which was thoroughly tested. You can learn more about it here: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94

Resources