When you access your Lambda function via API Gateway, you may want to control that access. For example, the following cases can be considered.
--I want to make Lambda executable only if I know a specific token that I shared in advance --When you pass a token obtained from the outside by an external IDaaS service (Auth0, Firebase Authentication, etc.) to the API, you want to verify the token and make Lambda executable if it is correct.
To solve this problem, when calling a Lambda function via API Gateway, a mechanism is in place so that validation can be performed immediately before the request. This is called the Lambda Authorizer.
The figure in Lambda authorizer description page is quoted below. By calling the Lambda Auth Function in this figure before the Lambda function (which you want to restrict access to) and approving it, you can restrict access to the latter function without writing a particularly large access control.
If you want to use an existing Lambda function as a Lambda Authorizer, chalice calls it a Custom Authorizer. here,
--Create a Lambda Auth Function using Chalice to authenticate your Firebase Authentication JWT token --Use the created Lambda Auth Function as a CustomAuthorizer from another Chalice project
Two methods will be described.
Build-in Authorizer Lambda Auth Function for Firebase Authentication
--You have already created a user, and you can get a JWT token at the client site.
--Use the JWT token obtained by ʻuser.getIdToken (true) in the ʻonAuthStateChanged
event.
- https://firebase.google.com/docs/auth/web/manage-users?hl=ja
--You have a json file (= private key) for firebase-admin
- https://firebase.google.com/docs/admin/setup?hl=ja
--You can get the json file by pressing the ** "Generate new private key" button from within the service account **
――Honestly, it took me a long time to understand this because the private key and the json file are not linked at all. What happened to the name here ...
Create one project for Lambda Auth Function. I will omit the installation etc., but here you should read it as if you installed chalice in a place that can be used globally (you can replace it with the state where virtualenv is activated).
#Create a new project
$ chalice new-project authorizer
$ cd authorizer
# firebase-Put admin in vendor and don't build locally on deploy
#In my environment it took 7 minutes to deploy, so I'm doing it to save time
$ mkdir vendor
$ pip install firebase-admin -t vendor
# firebase-Place the json file for admin
#Fixed files will not be uploaded unless they are placed in chalicelib
#Lambda goes up to S3, so if you are uncertain, it is better to encrypt and decrypt with MKS
$ mkdir chalicelib
$ cp "<firebase-admin settings json>" chalicelib/firebase-adminsdk-dev.json
Main files etc.
authorizer
├── app.py
├── .chalice
│ └── config.json
├── chalicelib
│ └── firebase-adminsdk-dev.json
└── vendor
└── (Lots of installed)
Now, write the main code as follows. This time we will deploy with stage = dev, but rewrite as needed.
json:.chalice/config.json
{
"version": "2.0",
"app_name": "authorizer",
"stages": {
"dev": {
"api_gateway_stage": "api",
"environment_variables": {
"FIREBASE_CONFIGFILE": "firebase-adminsdk-dev.json"
}
}
}
}
app.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import logging
from chalice import Chalice, AuthResponse
import firebase_admin
import firebase_admin.auth as firebase_auth
logger = logging.getLogger()
app = Chalice(app_name='authorizer')
#Load the settings and firebase-Initialize admin
firebase_cred = firebase_admin.credentials.Certificate(
os.path.join(
os.path.dirname(__file__), 'chalicelib',
os.environ['FIREBASE_CONFIGFILE']))
firebase_admin.initialize_app(firebase_cred)
@app.authorizer()
def authorizer(auth_request):
'''
The entity of the custom authorizer.
Specify Lambda for this entity from another Chalice Project.
'''
#Pass the JWT token obtained from Firebase Auth in the Authorization header
# curl -s '<API URL>' -H 'Authorization: <JWT Token>' | jq .
try:
jwt_token = auth_request.token
crimes = firebase_auth.verify_id_token(jwt_token)
context = dict(uid=crimes['uid'])
return AuthResponse(routes=['*'], principal_id=crimes['uid'], context=context)
except Exception as e:
logger.exception(e)
return AuthResponse(routes=[], principal_id='deny')
@app.route('/', authorizer=authorizer)
def index():
'''
For verification and for deploying the authorizer.
If there is no more than one route, the custom authorizer will not be deployed either.
'''
return { 'AuthContext': app.current_request.context }
Create a function that processes the JWT token with the @ app.authorizer ()
decorator and returns ʻAuthResponsedepending on the result. In AuthResponse, create "route accessible by the corresponding token (API Gateway level path)",
principal_id that uniquely identifies the user, and
context` that you want to acquire additionally at the time of authentication and pass it to the subsequent function. And include these.
For details on what kind of AuthResponse should be created and returned, refer to the following documents.
Also note that ** @ app.authorizer ()
requires parentheses **. Without parentheses, it becomes a different thing and does not work well.
Deploy
This time deploy with chalice deploy
.
If you want, you can do a chalice package
and then throw it into CloudFormation for deployment.
#Test if it works locally
# @app.authorizer()Only in the case of, it also works locally
#Note that other Authorizers do not work locally
$ chalice local
#Deploy to AWS
$ chalice deploy --profile chalice
Creating deployment package.
Creating IAM role: authorizer-dev-api_handler
Creating lambda function: authorizer-dev
Creating IAM role: authorizer-dev-authorizer
Creating lambda function: authorizer-dev-authorizer
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:ap-northeast-1:************:function:authorizer-dev
- Lambda ARN: arn:aws:lambda:ap-northeast-1:************:function:authorizer-dev-authorizer
- Rest API URL: https://**********.execute-api.ap-northeast-1.amazonaws.com/api/
So that's all the deployment work.
Testing
Check if you can actually access it. It's not reproducible, but sometimes it's not accessible for a while after deploying, so if that happens (= `` is returned), please wait a moment and try again.
#No Authorization header
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ | jq .
{
"message": "Unauthorized"
}
#If authentication fails routes=[]And the endpoint/Cannot access
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ -H 'Authorization: hoge'| jq .
{
"Message": "User is not authorized to access this resource"
}
#If you pass a JWT token, you can access it normally
# AuthContext.The value passed in context comes out to authorizer
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ -H 'Authorization: <Firebase Auth JWT Token>' | jq .
{
"AuthContext": {
"resourceId": "........",
"authorizer": {
"uid": "**********",
"principalId": "**********",
"integrationLatency": 190
},
"resourcePath": "/",
"httpMethod": "GET",
"extendedRequestId": "*************",
"requestTime": "07/May/2020:00:33:11 +0000",
"path": "/api/",
... (Omission) ...
}
}
As you can see, we were able to implement Authorizer in a very easy way.
Firebase Cost and Authorization Caching
If nothing is set, the Authorizer function will be called each time Lambda is accessed.
** All authentication other than Firebase Authorization phone authentication is free according to the price list **, but it is a little to call Lambda There is a charge.
Therefore, the cache of the authentication result can be used for a certain period of time. This can also be seen in "Policy is cached" in the first figure.
To enable the cache, specify ttl_seconds
as follows:
However, please note that the setting here is the setting in API Gateway, so it is not related to CustomAuthorizer described later.
Excerpt from authorizer implementation
@app.authorizer(ttl_seconds=120)
def authorizer(auth_request):
....
Build-in Authorizer vs Customer Authorizer
Writing your own logic in one project and performing authentication is called Build-in Authorizer in Chalice.
On the other hand, instead you can use the Authorizer prepared by Chalice. These include IAMAuthorizer, CognitoUserPoolAuthorizer, and CustomAuthorizer, which authenticate using existing AWS resources. Of these, CustomAuthorizer
is an Authorizer for using existing Lambda functions as Authorizers.
If you want to complete it as a simple task, you can use Build-in Authorier, but the firebase-admin library alone has a capacity of nearly 10MB (about 20MB when a binary build runs). I don't want to consume Lambda's capacity for a library that is used only in one place for authentication, so let's consider cutting out this part as a CustomAuthorizer as another Lambda function.
As I wrote in the comment of ʻapp.py, the Lambda function is enough to proceed from here. However, even if you write only
@ app.authorier, it will not be deployed, so if it is true, it is better to adopt the method of creating a
chalice package`, deleting unnecessary resources, and then deploying only the function. It may be good.
Another Chalice Project with CustomAuthorizer
Make a note of the arn of the authorizer function created in the ʻauthorizer project you created earlier. In this case, I think it looks like ʻarn: aws: lambda: <region>: <aws-account-no>: function: authorizer-dev-authorizer
.
Main files etc.
sample
└── app.py
app.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
from chalice import Chalice, CustomAuthorizer
app = Chalice(app_name='sample')
# authorizer_The uri part is.chalice/config.You may cut out to json
region = 'ap-northeast-1'
lambda_arn = '<ARN of the authorizer function created earlier>'
authorizer_uri = f'arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations'
# ttl_If there is no seconds, it defaults to a 300 second cache
authorizer = CustomAuthorizer(
'FirebaseAuthorizer',
ttl_seconds=60,
authorizer_uri=authorizer_uri)
@app.route('/private', authorizer=authorizer)
def private_function():
return {'RequestContext': app.current_request.context}
@app.route('/public')
def public_function():
return {'message': 'success'}
Create a CustomAuthorizer instance as described above. You can enter any name for the first argument. authorizer_uri specifies the Lambda function to call as Authorizer in the above format.
The URI of the lambda function to use for the custom authorizer. This usually has the form arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations. Quote-https://chalice.readthedocs.io/en/latest/api.html#CustomAuthorizer.authorizer_uri
Run it locally with the chalice local
command to test it, but note that it is designed not to work locally except for Build-in Authorizer.
If you want to operate a specific user in the local environment, you can check the execution status by environment_variables of stage and inject the authorizer for local.
#Can be run normally without Authorizer
$ curl -s http://localhost:8000/public/ | jq .
{
"message": "success"
}
#CustomAuthorizer cannot be implemented locally
$ curl -s http://localhost:8000/private/ | jq .
{
"RequestContext": {
"httpMethod": "GET",
"resourcePath": "/private",
"identity": {
"sourceIp": "127.0.0.1"
},
"path": "/private/"
}
}
#A message similar to the following is displayed in chalice local
# UserWarning: CustomAuthorizer is not a supported in local mode. All requests made against a route will be authorized to allow local testing.
Testing
After deploying with chalice deploy --profile chalice
, I check in order as before, but the behavior becomes strange from the moment I insert the ʻAuthorizationheader. Here you should see
"Message ":" User is not authorized to access this resource "`.
#You can access the public
$ curl -s https://********.execute-api.ap-northeast-1.amazonaws.com/api/public/ | jq .
{
"message": "success"
}
#Private cannot be accessed without authentication
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ | jq .
{
"message": "Unauthorized"
}
# !?!?
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ -H 'Authorization: hoge'| jq .
{
"message": null
}
Looking into this, the Chalice formula reports a similar problem.
Authorizer won't work on after deployment. - https://github.com/aws/chalice/issues/670
The solution says, "Open API Gateway in Management Console and overwrite the authorizer to use it." In fact, this will allow you to use it.
The reason is that the deployment using CustomAuthorizer alone cannot give ** a resource-based policy ** to call an existing Authorizer Lambda from the default API Gateway (Lambda specified by CustomAuthorizer is the current Chalice project). It's a completely unrelated resource, so it's certainly not good to make changes to that resource). If you overwrite the authorizer by the above procedure, you can automatically grant the Lambda resource-based policy. However, please note that if you change the name of the authorizer, "Delete ⇒ Generate" will be performed and the ID of the authorizer will be changed to another one, so the authority given to the existing Lambda will be lost. If you do not change the name, the ID will not change.
When not using Management Console ʻaws lambda add-permission` etc. allows existing functions to be called from API Gateway so that they can be called normally.
add-An example of permission
$ aws lambda add-permission \
--function-name authorizer-dev-authorizer \
--action lambda:InvokeFunction \
--statement-id <Enter the appropriate UID> \
--principal apigateway.amazonaws.com
However, in the case of this example, calls from any API Gateway are allowed, so if you want to be more strict, please specify Condition.
Re-Testing
#You can access the public
$ curl -s https://********.execute-api.ap-northeast-1.amazonaws.com/api/public/ | jq .
{
"message": "success"
}
#Private cannot be accessed without authentication
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ | jq .
{
"message": "Unauthorized"
}
#If authentication fails routes=[]And the endpoint/Cannot access
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ -H 'Authorization: hoge'| jq .
{
"Message": "User is not authorized to access this resource"
}
#If you pass a JWT token, you can access it normally
# AuthContext.The value passed in context comes out to authorizer
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ -H 'Authorization: <Firebase Auth JWT Token>' | jq .
{
"AuthContext": {
"resourceId": "........",
"authorizer": {
"uid": "**********",
"principalId": "**********",
"integrationLatency": 153
},
"resourcePath": "/private",
"httpMethod": "GET",
"extendedRequestId": "**************",
"requestTime": "07/May/2020:02:12:29 +0000",
"path": "/api/private/",
... (Omission) ...
}
}
There are some pitfalls in setting permissions when using CustomAuthorizer, but otherwise I was able to work with Firebase Authentication with very short code.
Since I use Firebase, I think there will be no trouble if it is completed with Firebase in the first place. Or, if you use AWS, why not use Cognito? It's not surprising, but I hope it will be helpful for people like me who want to do it on AWS, and want to use Firebase Authentication, which provides a free UI other than phone authentication, as the basis of IDaaS. is.
Recommended Posts