I'm new to loopback when I went through the documentation for authorization examples.
AuthorizationContext: contains current principal invoking an endpoint, request context, and expected roles and scopes.
Followed the same steps as mentioned in the doc, but while debugging I have found that AuthorizationContext.prinicpals is empty.
Here is the authorize provider
import {
AuthorizationContext, AuthorizationDecision, AuthorizationMetadata, Authorizer
} from '#loopback/authorization';
import {Provider} from '#loopback/core';
export class MyAuthorizationProvider implements Provider<Authorizer> {
constructor() {}
/**
* #returns authenticateFn
*/
value(): Authorizer {
return this.authorize.bind(this);
}
async authorize(
authorizationCtx: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
console.log(authorizationCtx);
const clientRole = authorizationCtx.principals[0].role;
const allowedRoles = metadata.allowedRoles;
return allowedRoles?.includes(clientRole)
? AuthorizationDecision.ALLOW
: AuthorizationDecision.DENY;
}
}
Related
I am trying to get a Cloud Function to create a Cloud Task that will invoke a Cloud Function. Easy.
The flow and use case are very close to the official tutorial here.
I also looked at this article by Doug Stevenson and in particular its security section.
No luck, I am consistently getting a 16 (UNAUTHENTICATED) error in Cloud Task.
If I can trust what I see in the console it seems that Cloud Task is not attaching the OIDC token to the request:
Yet, in my code I do have the oidcToken object:
const { v2beta3, protos } = require("#google-cloud/tasks");
import {
PROJECT_ID,
EMAIL_QUEUE,
LOCATION,
EMAIL_SERVICE_ACCOUNT,
EMAIL_HANDLER,
} from "./../config/cloudFunctions";
export const createHttpTaskWithToken = async function (
payload: {
to_email: string;
templateId: string;
uid: string;
dynamicData?: Record<string, any>;
},
{
project = PROJECT_ID,
queue = EMAIL_QUEUE,
location = LOCATION,
url = EMAIL_HANDLER,
email = EMAIL_SERVICE_ACCOUNT,
} = {}
) {
const client = new v2beta3.CloudTasksClient();
const parent = client.queuePath(project, location, queue);
// Convert message to buffer.
const convertedPayload = JSON.stringify(payload);
const body = Buffer.from(convertedPayload).toString("base64");
const task = {
httpRequest: {
httpMethod: protos.google.cloud.tasks.v2.HttpMethod.POST,
url,
oidcToken: {
serviceAccountEmail: email,
audience: new URL(url).origin,
},
headers: {
"Content-Type": "application/json",
},
body,
},
};
try {
// Send create task request.
const request = { parent: parent, task: task };
const [response] = await client.createTask(request);
console.log(`Created task ${response.name}`);
return response.name;
} catch (error) {
if (error instanceof Error) console.error(Error(error.message));
return;
}
};
When logging the task object from the code above in Cloud Logging I can see that the service account is the one that I created for the purpose of this and that the Cloud Tasks are successfully created.
IAM:
And the function that the Cloud Task needs to invoke:
Everything seems to be there, in theory.
Any advice as to what I would be missing?
Thanks,
Your audience is incorrect. It must end by the function name. Here, you only have the region and the project https://<region>-<projectID>.cloudfunction.net/. Use the full Cloud Functions URL.
Hi guys I struck with this for a days and I hope some of you can figure it out
I have lambda authorizer with simple response mode like this.
I want to forward value inside JWT token (user_id) to my Flask API.
My authorizer work fine but I don't know to access authorizer.claims in the Flask API.
exports.handler = async(event) => {
const method = 'main';
log.info(`[${method}] BEGIN`);
log.info(`[${method}] incoming event= ${JSON.stringify(event)}`);
const secret = await secretManager(secretName);
const response = {
isAuthorized: false,
context: {
authorizer: {
claims: {}
},
error: {}
}
};
const authorization = event.headers.authorization;
if(!authorization){
log.error(`[${method}] No authorization found`);
response.context.error.message = 'No authorization found';
response.context.error.responseType = 401;
return response;
}
const auth = authorization.split(' ');
if (auth[0] !== 'Bearer' || auth.length !== 2) {
log.error(`[${method}] Invalid authorization`);
response.context.error.message = 'Invalid authorization';
response.context.error.responseType = 401;
return response;
}
try {
const decoded = verify(auth[1], secret.public_key, {
algorithms: ['RS512'],
});
response.isAuthorized = true;
response.context.authorizer.claims = decoded;
} catch (error) {
log.error(`[${method}] Error occured:${error.message}`);
response.isAuthorized = false;
response.context.error.message = error.message;
response.context.error.responseType = 401;
}
log.info(`[${method}] END`);
return response;
};
And my Flask application be like this.
I used blueprint and app.before_request_funcs as a middlewares.
from api.middlewares.fetchUser import fetchUser
from api.routes.blueprint import myBlueprint
app.before_request_funcs = {
myBlueprint.name: [fetchUser]
}
app.register_blueprint(myBlueprint)
Here is where I want to access authorizer.claims to fetch user before my API start.
from flask import request, g
from api.utils.logger import logger
import json
def fetchUser():
logger.warn('FETCH USER')
logger.warn(json.dumps(request.headers.to_wsgi_list()))
logger.warn(json.dumps(request.authorization))
How to access authorizer.claims
Thanks you.
I have two lambda functions .
Now I want to use one api for these two.
Then my code is like this
const api = new apigateway.RestApi(this, 'ServerlessRestApi', {
restApiName: `AWSCDKTest-${systemEnv}`,
cloudWatchRole: false
});
api.root.addMethod('GET', new apigateway.LambdaIntegration(helloLambda));
api.root.addMethod('GET', new apigateway.LambdaIntegration(happyLambda));
Howeber it says GET is doubled.
So I made two API
const api = new apigateway.RestApi(this, 'ServerlessRestApi_hello', {
restApiName: `AWSCDK-Viral-${systemEnv}`,
cloudWatchRole: false
});
api.root.addMethod('GET', new apigateway.LambdaIntegration(helloLambda));
const api2 = new apigateway.RestApi(this, 'ServerlessRestApi_happy', { cloudWatchRole: false });
api2.root.addMethod('GET', new apigateway.LambdaIntegration(happyLambda));
It works, but it makes two API.
What is the best practice to use one API for two lambda??
API root:
GET "https://example_api_endpoint/" invokes helloLambda then inside this lambda function call the AWS-SDK lambda API method invoke() to trigger the execution of happyLambda within the first invocation in sequence. Otherwise, you cannot have two lambda functions for a single API resource. Further reading: AWS Lambda Fanout pattern.
One lambda cannot be linked to exact same API path + Http verb. Here are some alternatives -
a) have different api paths, each calling different lambda
b) have one lambda triggered by API gateway, which triggers other lambda that you need (within it's code)
c) have the lambda integration to the endpoint that sends a message to SNS, and have multiple lambda subscribe to the SNS via SQS.
try this
this.api = new RestApi(this, 'ServerlessRestApi', {
restApiName: "myapi",
cloudWatchRole: false
});
this.api.root.addResource(resource).addMethod('GET', new LambdaIntegration(func));
try this
// ./helloService.helloLambda.js file sample
export const handler = async (event, context) => {
return {
statusCode: 200,
headers: {},
body: 'helloLambda',
};
};
// ./helloService.happyLambda.js file sample
export const handler = async (event, context) => {
return {
statusCode: 200,
headers: {},
body: 'happyLambda',
};
};
// .helloService file sample
import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
export class HelloService extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const helloLambdaFunction = new NodejsFunction(this, 'helloLambda');
const happyLambdaFunction = new NodejsFunction(this, 'happyLambda');
const api = new apigateway.RestApi(this, 'hello-api', {
description: 'This service is Happy.',
});
const helloLambdaPath = api.root.addResource('helloLambda');
// path name https://{createdId}.execute-api.{region}.amazonaws.com/prod/helloLambda
helloLambdaPath.addMethod('GET', new apigateway.LambdaIntegration(helloLambdaFunction));
const happyLambdaPath = api.root.addResource('happyLambda');
// path name https://{createdId}.execute-api.{region}.amazonaws.com/prod/happyLambda
happyLambdaPath.addMethod('GET', new apigateway.LambdaIntegration(happyLambdaFunction));
}
}
My client has a GraphQL API running on Google cloud run.
I have recieved a service account for authentication as well as access to the gcloud command line tool.
When using gcloud command line like so:
gcloud auth print-identity-token
I can generate a token that can be used to make post requests to the api. This works and I can make successful post requests to the api from postman, insomnia and from my nodejs app.
However, when I use JWT authentication with "googleapis" or "google-auth" npm libraries like so :
var { google } = require('googleapis')
let privatekey = require('./auth/google/service-account.json')
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
['https://www.googleapis.com/auth/cloud-platform']
)
jwtClient.authorize(function(err, _token) {
if (err) {
console.log(err)
return err
} else {
console.log('token obj:', _token)
}
})
This outputs a "bearer" token:
token obj: {
access_token: 'ya29.c.Ko8BvQcMD5zU-0raojM_u2FZooWMyhB9Ni0Yv2_dsGdjuIDeL1tftPg0O17uFrdtkCuJrupBBBK2IGfUW0HGtgkYk-DZiS1aKyeY9wpXTwvbinGe9sud0k1POA2vEKiGONRqFBSh9-xms3JhZVdCmpBi5EO5aGjkkJeFI_EBry0E12m2DTm0T_7izJTuGQ9hmyw',
token_type: 'Bearer',
expiry_date: 1581954138000,
id_token: undefined,
refresh_token: 'jwt-placeholder'
}
however this bearer token does not work as the one above and always gives an "unauthorised error 401" when making the same requests as with the gcloud command "gcloud auth print-identity-token".
Please help, I am not sure why the first bearer token works but the one generated with JWT does not.
EDIT
I have also tried to get an identity token instead of an access token like so :
let privatekey = require('./auth/google/service-account.json')
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
[]
)
jwtClient
.fetchIdToken('https://my.audience.url')
.then((res) => console.log('res:', res))
.catch((err) => console.log('err', err))
This prints an identity token, however, using this also just gives a "401 unauthorised" message.
Edit to show how I am calling the endpoint
Just a side note, any of these methods below work with the command line identity token, however when generated via JWT, it returns a 401
Method 1:
const client = new GraphQLClient(baseUrl, {
headers: {
Authorization: 'Bearer ' + _token.id_token
}
})
const query = `{
... my graphql query goes here ...
}`
client
.request(query)
.then((data) => {
console.log('result from query:', data)
res.send({ data })
return 0
})
.catch((err) => {
res.send({ message: 'error ' + err })
return 0
})
}
Method 2 (using the "authorized" client I have created with google-auth):
const res = await client.request({
url: url,
method: 'post',
data: `{
My graphQL query goes here ...
}`
})
console.log(res.data)
}
Here is an example in node.js that correctly creates an Identity Token with the correct audience for calling a Cloud Run or Cloud Functions service.
Modify this example to fit the GraphQLClient. Don't forget to include the Authorization header in each call.
// This program creates an OIDC Identity Token from a service account
// and calls an HTTP endpoint with the Identity Token as the authorization
var { google } = require('googleapis')
const request = require('request')
// The service account JSON key file to use to create the Identity Token
let privatekey = require('/config/service-account.json')
// The HTTP endpoint to call with an Identity Token for authorization
// Note: This url is using a custom domain. Do not use the same domain for the audience
let url = 'https://example.jhanley.dev'
// The audience that this ID token is intended for (example Google Cloud Run service URL)
// Do not use a custom domain name, use the Assigned by Cloud Run url
let audience = 'https://example-ylabperdfq-uc.a.run.app'
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
audience
)
jwtClient.authorize(function(err, _token) {
if (err) {
console.log(err)
return err
} else {
// console.log('token obj:', _token)
request(
{
url: url,
headers: {
"Authorization": "Bearer " + _token.id_token
}
},
function(err, response, body) {
if (err) {
console.log(err)
return err
} else {
// console.log('Response:', response)
console.log(body)
}
}
);
}
})
You can find the official documentation for node OAuth2
A complete OAuth2 example:
const {OAuth2Client} = require('google-auth-library');
const http = require('http');
const url = require('url');
const open = require('open');
const destroyer = require('server-destroy');
// Download your OAuth2 configuration from the Google
const keys = require('./oauth2.keys.json');
/**
* Start by acquiring a pre-authenticated oAuth2 client.
*/
async function main() {
const oAuth2Client = await getAuthenticatedClient();
// Make a simple request to the People API using our pre-authenticated client. The `request()` method
// takes an GaxiosOptions object. Visit https://github.com/JustinBeckwith/gaxios.
const url = 'https://people.googleapis.com/v1/people/me?personFields=names';
const res = await oAuth2Client.request({url});
console.log(res.data);
// After acquiring an access_token, you may want to check on the audience, expiration,
// or original scopes requested. You can do that with the `getTokenInfo` method.
const tokenInfo = await oAuth2Client.getTokenInfo(
oAuth2Client.credentials.access_token
);
console.log(tokenInfo);
}
/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the full client to the callback.
*/
function getAuthenticatedClient() {
return new Promise((resolve, reject) => {
// create an oAuth client to authorize the API call. Secrets are kept in a `keys.json` file,
// which should be downloaded from the Google Developers Console.
const oAuth2Client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret,
keys.web.redirect_uris[0]
);
// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/userinfo.profile',
});
// Open an http server to accept the oauth callback. In this simple example, the
// only request to our webserver is to /oauth2callback?code=<code>
const server = http
.createServer(async (req, res) => {
try {
if (req.url.indexOf('/oauth2callback') > -1) {
// acquire the code from the querystring, and close the web server.
const qs = new url.URL(req.url, 'http://localhost:3000')
.searchParams;
const code = qs.get('code');
console.log(`Code is ${code}`);
res.end('Authentication successful! Please return to the console.');
server.destroy();
// Now that we have the code, use that to acquire tokens.
const r = await oAuth2Client.getToken(code);
// Make sure to set the credentials on the OAuth2 client.
oAuth2Client.setCredentials(r.tokens);
console.info('Tokens acquired.');
resolve(oAuth2Client);
}
} catch (e) {
reject(e);
}
})
.listen(3000, () => {
// open the browser to the authorize url to start the workflow
open(authorizeUrl, {wait: false}).then(cp => cp.unref());
});
destroyer(server);
});
}
main().catch(console.error);
Edit
Another example for cloud run.
// sample-metadata:
// title: ID Tokens for Cloud Run
// description: Requests a Cloud Run URL with an ID Token.
// usage: node idtokens-cloudrun.js <url> [<target-audience>]
'use strict';
function main(
url = 'https://service-1234-uc.a.run.app',
targetAudience = null
) {
// [START google_auth_idtoken_cloudrun]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const url = 'https://YOUR_CLOUD_RUN_URL.run.app';
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
if (!targetAudience) {
// Use the request URL hostname as the target audience for Cloud Run requests
const {URL} = require('url');
targetAudience = new URL(url).origin;
}
console.info(
`request Cloud Run ${url} with target audience ${targetAudience}`
);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
request().catch(err => {
console.error(err.message);
process.exitCode = 1;
});
// [END google_auth_idtoken_cloudrun]
}
const args = process.argv.slice(2);
main(...args);
For those of you out there that do not want to waste a full days worth of work because of the lack of documentation. Here is the accepted answer in today's world since the JWT class does not accept an audience in the constructor anymore.
import { JWT } from "google-auth-library"
const client = new JWT({
forceRefreshOnFailure: true,
key: service_account.private_key,
email: service_account.client_email,
})
const token = await client.fetchIdToken("cloud run endpoint")
const { data } = await axios.post("cloud run endpoint"/path, payload, {
headers: {
Authorization: `Bearer ${token}`
}
})
return data
In loopback4, I have created custom authentication and authorization handlers, and wired them into the application. But the authorization handler is called only if the authentication function returns a UserProfile object, and skips authorization for an undefined user.
I want my Authorization handler to be called every time, no matter what the result of authentication is. I want to allow a non-authenticated call (don't know the user) to still flow through the authorization handler to let it judge whether to allow the call based on other factors besides the identity of the end user.
How do I make the Authorization handler be called every time?
export class MySequence implements SequenceHandler {
constructor(
#inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
#inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
#inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
#inject(SequenceActions.SEND) public send: Send,
#inject(SequenceActions.REJECT) public reject: Reject,
#inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) {}
// see: https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#adding-an-authentication-action-to-a-custom-sequence
async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
//call authentication action
console.log(`request path = ${request.path}`);
await this.authenticateRequest(request); // HOW DO I CONTROL AUTHORIZATION CALL THAT FOLLOWS?
// Authentication step done, proceed to invoke controller
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
if (
error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
error.code === USER_PROFILE_NOT_FOUND
) {
Object.assign(error, {statusCode: 401 /* Unauthorized */});
}
this.reject(context, error);
}
}
}
The full example of code is lengthy, so I have posted it in a gist here.
I found one way to invoke an authorization handler for every request. This still doesn't feel quite right, so there's probably a better solution.
In the application.ts you can setup default authorization metadata and supply a simpler voter that always votes DENY. After that, all controller calls will invoke authorization handlers, whether there is a #authorize() decorator present or not. Here's the setup:
// setup authorization
const noWayJose = (): Promise<AuthorizationDecision> => {
return new Promise(resolve => {
resolve(AuthorizationDecision.DENY);
});
};
this.component(AuthorizationComponent);
this.configure(AuthorizationBindings.COMPONENT).to({
defaultDecision: AuthorizationDecision.DENY,
precedence: AuthorizationDecision.ALLOW,
defaultMetadata: {
voters: [noWayJose],
},
});
this.bind('authorizationProviders.my-authorization-provider')
.toProvider(MyAuthorizationProvider)
.tag(AuthorizationTags.AUTHORIZER);
Now the /nope endpoint in the controller will have Authorization handlers evaluated even without the decorator.
export class YoController {
constructor() {}
#authorize({scopes: ['IS_COOL', 'IS_OKAY']})
#get('/yo')
yo(#inject(SecurityBindings.USER) user: UserProfile): string {
return `yo, ${user.name}!`;
}
#authorize({allowedRoles: [EVERYONE]})
#get('/sup')
sup(): string {
return `sup, dude.`;
}
#get('/nope')
nope(): string {
return `sorry dude.`;
}
#authorize({allowedRoles: [EVERYONE]})
#get('/yay')
yay(
#inject(SecurityBindings.USER, {optional: true}) user: UserProfile,
): string {
if (user) {
return `yay ${user.name}!`;
}
return `yay!`;
}
}
The other thing you have to do is not throw an error when authentication fails to find a user. That's because authorization does not get exercised until the invoke() function calls all the interceptors. So you have to swallow that error and let authorization have a say:
// from sequence.ts
async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
//call authentication action
console.log(`request path = ${request.path}`);
try {
await this.authenticateRequest(request);
} catch (authenticationError) {
if (authenticationError.code === USER_PROFILE_NOT_FOUND) {
console.log(
"didn't find user. let's wait and see what authorization says.",
);
} else {
throw authenticationError;
}
}
// Authentication step done, proceed to invoke controller
const args = await this.parseParams(request, route);
// Authorization happens within invoke()
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
if (
error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
error.code === USER_PROFILE_NOT_FOUND
) {
Object.assign(error, {statusCode: 401 /* Unauthorized */});
}
this.reject(context, error);
}
}
This is all suited to my use case. I wanted global defaults to have every endpoint be locked down with zero #authenticate and #authorize() decorators present. I plan to only add #authorize() to those places where I want to open things up. This is because I'm about to auto-generate a ton of controllers and will only want to expose a portion of the endpoints by hand.