[PYTHON] Implémenter l'autorisation personnalisée pour l'authentification Firebase dans Chalice

introduction

Lors de l'accès aux fonctions Lambda via API Gateway, vous souhaiterez peut-être contrôler cet accès. Par exemple, les cas suivants peuvent être envisagés.

--Je souhaite rendre Lambda exécutable uniquement si je connais un jeton spécifique que j'ai partagé à l'avance --Lorsque vous transmettez un token obtenu de l'extérieur par un service IDaaS externe (Auth0, Firebase Authentication, etc.) à l'API, vous souhaitez vérifier le token et rendre Lambda exécutable s'il est correct.

Afin de résoudre ce problème, lors de l'appel d'une fonction Lambda via API Gateway, un mécanisme est en place afin que la vérification puisse être effectuée immédiatement avant la demande. C'est ce qu'on appelle le Lambda Authorizer.

Le chiffre de la Page de description de Lambda Authorizer est cité ci-dessous. En appelant la fonction d'authentification Lambda dans cette figure avant la fonction Lambda (à laquelle vous souhaitez restreindre l'accès) et en l'approuvant, vous pouvez restreindre l'accès à cette dernière fonction sans écrire un contrôle d'accès particulièrement important.

custom-auth-workflow.png

Lorsque vous utilisez une fonction Lambda existante en tant qu'autorisateur Lambda, Calice l'appelle un autorisateur personnalisé. ici,

--Créez une fonction d'authentification Lambda à l'aide de Chalice pour authentifier le jeton JWT de l'authentification Firebase --Utilisez la fonction d'authentification Lambda créée en tant qu'autorisateur personnalisé à partir d'un autre projet Chalice

Deux méthodes seront décrites.

Build-in Authorizer Lambda Auth Function for Firebase Authentication

Préparation préalable

Paramètres du calice

Créez un projet pour la fonction d'authentification Lambda. Je vais omettre l'installation, etc., mais ici, lisez-la comme si vous aviez installé calice dans un endroit qui peut être utilisé globalement (vous pouvez le remplacer par l'état où virtualenv est activé).

#Créer un nouveau projet
$ chalice new-project authorizer
$ cd authorizer

# firebase-Mettez l'administrateur dans le fournisseur et ne construisez pas localement lors du déploiement
#Le déploiement dans mon environnement a pris 7 minutes, donc je le fais pour gagner du temps
$ mkdir vendor
$ pip install firebase-admin -t vendor

# firebase-Placer le fichier json pour l'administrateur
#Les fichiers fixes ne seront pas téléchargés sauf s'ils sont placés dans chalicelib
#Lambda passe à S3, donc si vous êtes inquiet, il vaut mieux chiffrer et déchiffrer avec MKS.
$ mkdir chalicelib
$ cp "<firebase-paramètres d'administration json>" chalicelib/firebase-adminsdk-dev.json

Fichiers principaux etc.


authorizer
├── app.py
├── .chalice
│   └── config.json
├── chalicelib
│   └── firebase-adminsdk-dev.json
└── vendor
    └── (Beaucoup d'installé)

Maintenant, écrivez le code principal comme suit. Cette fois, déployez avec stage = dev, mais réécrivez si nécessaire.

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')

#Chargez les paramètres et la base de feu-Initialiser l'administrateur
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):
    '''
L'entité de l'autorisation personnalisée.
Spécifiez Lambda pour cette entité à partir d'un autre projet Chalice.
    '''
    #Passez le jeton JWT obtenu auprès de Firebase Auth dans l'en-tête d'autorisation
    # 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():
    '''
Pour la vérification et pour le déploiement de l'autorisation.
S'il n'y a pas une ou plusieurs routes, l'autorisation personnalisée ne sera pas déployée non plus.
    '''
    return { 'AuthContext': app.current_request.context }

@ app.authorizer () Crée une fonction qui traite les jetons JWT avec un décorateur et renvoie ʻAuthResponse` en fonction du résultat. Dans AuthResponse, créez "route accessible par le jeton correspondant (chemin de niveau API Gateway)", "principal_id" qui identifie de manière unique l'utilisateur et "contexte" que vous souhaitez acquérir en plus au moment de l'authentification et le transmettre à la fonction suivante. Et incluez-les.

Pour plus de détails sur le type d'AuthResponse à créer et à renvoyer, reportez-vous aux documents suivants.

Notez également que ** @ app.authorizer () nécessite des parenthèses **. Sans parenthèses, cela devient une chose différente et ne fonctionne pas bien.

Deploy

Cette fois, déployez avec Chalice deploy. Si nécessaire, vous pouvez créer un paquet calice, puis le lancer dans CloudFormation pour le déploiement.

#Testez si cela fonctionne localement
# @app.authorizer()Seulement dans le cas de, cela fonctionne également localement
#Notez que les autres autorisateurs ne fonctionnent pas localement
$ chalice local 

#Déployer sur 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/

C'est donc tout le travail de déploiement.

Testing

Vérifiez si vous pouvez réellement y accéder. Ce n'est pas reproductible, mais parfois il n'est pas accessible pendant un certain temps après le déploiement, donc si cela se produit (= `` est retourné), veuillez patienter un moment et réessayer.

#Aucun en-tête d'autorisation
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ | jq .
{
  "message": "Unauthorized"
}

#Si l'authentification échoue, les routes=[]Et le point final/Ne peut pas accéder
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ -H 'Authorization: hoge'| jq .
{
  "Message": "User is not authorized to access this resource"
}

#Si vous transmettez le jeton JWT, vous pouvez accéder normalement
# AuthContext.La valeur passée en contexte sort de l'autorisateur
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/ -H 'Authorization: <Jeton Firebase Auth JWT>' | jq .
{
  "AuthContext": {
    "resourceId": "........",
    "authorizer": {
      "uid": "**********",
      "principalId": "**********",
      "integrationLatency": 190
    },
    "resourcePath": "/",
    "httpMethod": "GET",
    "extendedRequestId": "*************",
    "requestTime": "07/May/2020:00:33:11 +0000",
    "path": "/api/",
    ... (Omission) ...
  }
}

Comme vous pouvez le voir, nous avons pu implémenter Authorizer de manière très simple.

Firebase Cost and Authorization Caching

Si rien n'est défini, la fonction Authorizer sera appelée à chaque accès à Lambda.

** Toute authentification autre que l'authentification par téléphone Firebase Authorization est gratuite selon la liste de prix **, mais c'est un peu d'appeler Lambda Il y a des frais.

Par conséquent, le cache du résultat de l'authentification peut être utilisé pendant une certaine période de temps. Cela peut également être vu dans "La stratégie est mise en cache" dans la première figure.

custom-auth-workflow.png

Pour activer la mise en cache, spécifiez ttl_seconds comme suit: Cependant, veuillez noter que les paramètres ici sont des paramètres dans API Gateway, ils ne sont donc pas liés à l'autorisation personnalisée décrite plus loin.

Extrait de l'implémentation de l'autorisation


@app.authorizer(ttl_seconds=120)
def authorizer(auth_request):
    ....

Build-in Authorizer vs Customer Authorizer

L'écriture de votre propre logique dans un projet et l'exécution de l'authentification s'appelle Build-in Authorizer in Chalice. En revanche, vous pouvez utiliser l'Autorizer préparé par Chalice. Il s'agit notamment de IAMAuthorizer, CognitoUserPoolAuthorizer et CustomAuthorizer, qui utilisent les ressources AWS existantes pour l'authentification. Parmi ceux-ci, «CustomAuthorizer» est un Authorizer permettant d'utiliser des fonctions Lambda existantes en tant qu'autoriseurs.

Si vous voulez le terminer comme une tâche simple, vous pouvez utiliser Build-in Authorier, mais la bibliothèque firebase-admin a à elle seule une capacité de près de 10 Mo (environ 20 Mo lorsqu'une construction binaire s'exécute). Je ne veux pas consommer la capacité de Lambda pour une bibliothèque qui n'est utilisée qu'à un seul endroit pour l'authentification, nous allons donc envisager de supprimer cette partie en tant que CustomAuthorizer en tant qu'autre fonction Lambda.

Comme je l'ai écrit dans le commentaire de ʻapp.py, la fonction Lambda suffit à partir d'ici. Cependant, même si vous n'écrivez que @ app.authorier, il ne sera pas déployé, donc si c'est vrai, il est préférable d'adopter la méthode de création du paquet calice`, de supprimer les ressources inutiles, puis de déployer uniquement la fonction. Cela peut être bon.

Another Chalice Project with CustomAuthorizer

Paramètres du calice

Notez l'arn de la fonction d'autorisation créée dans le projet ʻauthorizer créé précédemment. Dans ce cas, je pense que cela ressemble à ʻarn: aws: lambda: <région>: <aws-account-no>: function: authorizer-dev-authorizer.

Fichiers principaux 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_La partie uri est.chalice/config.Vous pouvez le couper en json
region = 'ap-northeast-1'
lambda_arn = '<ARN de la fonction d'autorisation créée précédemment>'
authorizer_uri = f'arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations'

# ttl_S'il n'y a pas de secondes, la valeur par défaut est un cache de 300 secondes.
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'}

Créez une instance CustomAuthorizer comme décrit ci-dessus. Vous pouvez entrer n'importe quel nom pour le premier argument. authorizer_uri spécifie la fonction Lambda à appeler en tant qu'autoriseur au format ci-dessus.

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. Citation-https: //chalice.readthedocs.io/en/latest/api.html#CustomAuthorizer.authorizer_uri

Exécutez-le localement avec la commande calice local pour le tester, mais notez qu'il est conçu pour ne pas fonctionner localement à l'exception de Build-in Authorizer. Si vous souhaitez faire fonctionner un utilisateur spécifique dans l'environnement local, vous pouvez vérifier l'état d'exécution avec les variables d'environnement de stage et injecter l'autorisation pour local.

#Peut être exécuté normalement sans Authorizer
$ curl -s http://localhost:8000/public/ | jq . 
{
  "message": "success"
}

#CustomAuthorizer ne peut pas être implémenté localement
$ curl -s http://localhost:8000/private/ | jq . 
{
  "RequestContext": {
    "httpMethod": "GET",
    "resourcePath": "/private",
    "identity": {
      "sourceIp": "127.0.0.1"
    },
    "path": "/private/"
  }
}

#Un message similaire au suivant s'affiche sur le calice local
# UserWarning: CustomAuthorizer is not a supported in local mode. All requests made against a route will be authorized to allow local testing.

Testing

Après avoir déployé avec calice deploy --profile calice, nous vérifions dans l'ordre comme avant, mais le comportement devient étrange à partir du moment où nous insérons l'en-tête ʻAuthorization. Ici, vous devriez voir "" Message ":" L'utilisateur n'est pas autorisé à accéder à cette ressource ".

#Vous pouvez accéder au public
$ curl -s https://********.execute-api.ap-northeast-1.amazonaws.com/api/public/ | jq .
{
  "message": "success"
}

#Accès privé impossible sans authentification
$ 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
}

Exécuter l’autoriseur personnalisé

En regardant cela, la formule Chalice signale un problème similaire.

Authorizer won't work on after deployment. - https://github.com/aws/chalice/issues/670

La solution dit: "Ouvrez API Gateway dans la console de gestion et écrasez l'autorisation pour l'utiliser." En fait, cela vous permettra de l'utiliser.

La raison en est que le déploiement utilisant CustomAuthorizer seul ne peut pas donner ** une stratégie basée sur les ressources pour appeler un Authorizer Lambda existant à partir de la passerelle API par défaut ** (Lambda spécifié par CustomAuthorizer est le projet Chalice actuel). C'est une ressource totalement indépendante, il n'est donc certainement pas bon d'apporter des modifications à cette ressource). Si vous écrasez l'autoriseur à l'aide de la procédure ci-dessus, vous pouvez attribuer automatiquement la stratégie basée sur les ressources de Lambda. Cependant, veuillez noter que si vous changez le nom de l’autorisateur, vous exécuterez «Supprimer ⇒ Générer» et l’ID de l’autorisateur changera en un autre, vous ne pourrez donc pas utiliser l’autorité donnée au Lambda existant. Si vous ne modifiez pas le nom, l'ID ne changera pas.

Lorsque vous n'utilisez pas la console de gestion, ʻaws lambda add-permission` etc. permet aux fonctions existantes d'être appelées à partir d'API Gateway afin qu'elles puissent être appelées normalement.

add-Un exemple de permission


$ aws lambda add-permission \
    --function-name authorizer-dev-authorizer \
    --action lambda:InvokeFunction \
    --statement-id <Entrez un UID approprié> \
    --principal apigateway.amazonaws.com

Cependant, dans le cas de cet exemple, les appels depuis n'importe quelle passerelle API sont autorisés, donc si vous voulez être plus strict, veuillez spécifier Condition.

Re-Testing

#Vous pouvez accéder au public
$ curl -s https://********.execute-api.ap-northeast-1.amazonaws.com/api/public/ | jq .
{
  "message": "success"
}

#Accès privé impossible sans authentification
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ | jq .
{
  "message": "Unauthorized"
}

#Si l'authentification échoue, les routes=[]Et le point final/Ne peut pas accéder
$ 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"
}

#Si vous transmettez le jeton JWT, vous pouvez accéder normalement
# AuthContext.La valeur passée en contexte sort de l'autorisateur
$ curl -s https://**********.execute-api.ap-northeast-1.amazonaws.com/api/private/ -H 'Authorization: <Jeton Firebase Auth JWT>' | 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) ...
  }
}

Résumé

Il y a quelques pièges dans la définition des autorisations lors de l'utilisation de CustomAuthorizer, mais sinon, j'ai pu travailler avec Firebase Authentication avec un code très court.

Puisque j'utilise Firebase, je pense qu'il n'y aura aucun problème s'il est complété avec Firebase en premier lieu. Ou, si vous utilisez AWS, pourquoi ne pas utiliser Cognito? Ce n'est pas surprenant, mais j'espère que cela sera utile pour les gens comme moi qui veulent le faire essentiellement sur AWS et qui souhaitent utiliser l'authentification Firebase, qui fournit également une interface utilisateur gratuite autre que l'authentification par téléphone, comme base d'IDaaS. est.

Recommended Posts

Implémenter l'autorisation personnalisée pour l'authentification Firebase dans Chalice
Implémenter GraphConvLayer de DeepChem dans la couche personnalisée de PyTorch
Implémenter un décorateur de vue personnalisé avec Pyramid
Implémentez GraphGatherLayer de DeepChem avec la couche personnalisée de PyTorch
Implémenter un modèle utilisateur personnalisé dans Django
[Implémentation pour l'apprentissage] Implémentation de l'échantillonnage stratifié en Python (1)
[Fast API + Firebase] Construction d'un serveur API pour l'authentification au porteur