[JAVA] Protégez les API REST créées avec Cloud Functions avec l'authentification Firebase. Et appliquez l'authentification Firebase à l'API de votre propre serveur REST.

Dernière fois appelé Cloud Functions for Firebase, une logique placée dans un environnement sans serveur, via HTTPS.

Maintenant, concernant l'appel HTTPS, le `` functions.https.onRequest '': utilisé à ce moment-là.

index.ts


export const echo = functions.https.onRequest((request, response) => {
  const task = request.body
  response.send(JSON.stringify(task))
})

En dehors de cela, il semble y avoir une méthode appelée `` functions.https.onCall '', et cette fois j'utiliserai cette méthode. Et avec cela comme point de départ, réfléchissons à la manière de protéger l'API REST basée sur Functions avec la fonction d'authentification de Firebase.

Au fait, j'aimerais intégrer l'authentification Firebase dans le serveur REST que j'ai moi-même construit.

TL;DR

Si vous créez un service REST avec `` functions.https.onRequest '', le contrôle d'authentification, etc. ne sera pas effectué automatiquement. Par conséquent, il est nécessaire de le créer, mais en plaçant le jeton d'ID Firebase dans l'en-tête d'autorisation, il ne peut être utilisé que par les utilisateurs authentifiés Firebase.

De plus, comme le jeton ID est envoyé au format JWT qui peut être vérifié pour altération, le côté fonction peut vérifier la validité en interrogeant Firebase, et l'ID utilisateur numéroté par Firebase peut être récupéré.

Vous pouvez demander à Firebase de vérifier la validité du jeton ID en insérant le SDK pour le serveur REST que vous avez créé vous-même, et même si vous ne pouvez pas insérer le SDK, vérifiez la validité avec la soi-disant bibliothèque de jetons JWT, etc. Est possible. J'ai essayé à la fois le SDK et moi-même, mais j'ai pensé qu'il serait plus facile de préparer une bibliothèque et de la vérifier moi-même si le paramètre d'environnement est plutôt difficile à mettre dans le SDK.

Cela ressemble à ceci sur la figure.

image.png

image.png

Je n'ai pas déployé l'application WEB sur le serveur cette fois, mais je l'ai écrite en supposant qu'elle sera déployée sur Firebase Hosting. De plus, la nécessité de prendre en charge CORS (Cross-Origin Resource Sharing) n'est pas correctement décrite dans cet article, mais onCall semble être compatible CORS.

Essayez d'abord sans authentification Firebase

Utilisons maintenant `` functions.https.onCall ''. Je souhaite ajouter des fonctionnalités à mon projet précédent, je vais donc récupérer mon projet précédent de Github.

$ git clone --branch restsamples000 https://github.com/masatomix/fb_function_samples.git
$ cd fb_function_samples/functions/
$ npm install
$ cd ../

$ firebase use --add   //Sélectionnez votre projet sur

Construisons maintenant en ajoutant ce qui suit à index.ts.

index.ts


export const echo_onCall = functions.https.onCall((data, context) => {
  console.log('data: ' + JSON.stringify(data))
  console.log('context.auth: ' + JSON.stringify(context.auth))
  if (context.auth) {
    console.log('context.auth.uid: ' + context.auth.uid)
  }
  return data
})

Construire:

$ firebase deploy --only functions

Appeler avec Curl depuis la ligne de commande

Tout d'abord, révisez. L'original `` const hello = functions.https.onRequest ((req, res) ... '' pourrait être appelé par la méthode suivante.

$ cat request.json
{
  "id": "001",
  "name": "Bonjour",
  "isDone": true
}

$ curl -X POST -H "Content-Type:application/json" \
  --data-binary  @request.json \
  https://us-central1-xxxxx-xxxxxxx.cloudfunctions.net/echo | jq

{
  "id": "001",
  "name": "Bonjour",
  "isDone": true
}
$ (Généralement en écho)

Au fait, `` const echo_onCall = functions.https.onCall ((data, context) ... '' Pour cela, il semble que le format de l'appel soit décidé dans une certaine mesure.

Le détail est

Vous pouvez l'appeler depuis curl comme suit.

$ cat post_data.json
{
  "data": { //Enveloppez-le dans les données,
    "id": "001",
    "name": "Bonjour",
    "isDone": true
  }
}




$ curl -X POST -H "Content-Type:application/json" \
  --data-binary @post_data.json \
  https://us-central1-xxxxx-xxxxxxx..cloudfunctions.net/echo_onCall | jq

{
  "result": { //Emballé dans le résultat et retourné
    "id": "001",
    "name": "Bonjour",
    "isDone": true
  }
}

La fonction nouvellement enregistrée renvoie simplement les données d'argument, mais parmi les données POSTées, la partie enveloppée dans la propriété data est extraite, définie dans la propriété result et renvoyée.

Eh bien, c'est difficile à comprendre, mais je comprends comment ça marche orz. ..

Construisez un projet Vue.js et essayez de l'appeler via le SDK

Au fait, `` functions.https.onCall ((data, context) ... '', mais lors de l'appel de cette fonction depuis le WEB, il semble qu'elle soit censée être appelée depuis le SDK Firebase (Firebase JavaScript SDK etc.) Droite.

Donc, ensuite, je l'appellerai via SDK en utilisant l'application construite avec Vue.js.

Remarques lors de l'utilisation approximative des principales fonctions de Vue.js (authentification / autorisation Firebase) À faire avec l'authentification Firebase J'ai une application donc je vais l'utiliser. Veuillez également activer l'authentification, etc. côté Firebase, conformément à cet article. J'utiliserai l'authentification plus tard.

Cliquez ici pour savoir comment créer une application Vue.js.

$ git clone --branch idToken001 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.Réécrire js selon vos propres paramètres

$ npm run dev

Maintenant que le serveur a démarré, essayez d'accéder à http: // localhost: 8080 / # / token /. image.png

Cet écran n'étant pas protégé par l'authentification Firebase, l'écran WEB avec le bouton s'affiche. Essayez de cliquer sur ce bouton. image.png

Certaines données ont été renvoyées.

À propos, cette source est la suivante.

Token.vue


<template>
  <main class="container">
    <div>Appel HTTP!</div>
    <button class="btn btn-primary" @click="checkToken">Appel HTTP!</button>
  </main>
</template>

<script>
import firebase from "firebase";

export default {
  name: "Token",
  methods: {
    checkToken() {
      if (firebase.auth().currentUser) {
        firebase.auth().currentUser.getIdToken()
          .then(token => console.log(token));
      } else {
        console.log("currentUser is null");
      }

      const value = {
        id: "001",
        name: "Bonjour",
        isDone: true
      };
      //Appel HTTP
      const echo_onCall = firebase.functions().httpsCallable("echo_onCall");
      echo_onCall(value).then(result => alert(JSON.stringify(result)));
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
  

La fonction echo_onCall enregistrée dans Functions of Firebase était censée renvoyer simplement l'argument, mais lorsque le bouton est pressé, la fonction const echo_onCall = firebase.functions (). HttpsCallable ("echo_onCall") du SDK , ʻEcho_onCall (valeur) ʻIl semble que la valeur transmise soit enveloppée dans la propriété data dans le rappel et retournée.

Regardons les télégrammes de haut en bas avec un navigateur.

Télégramme d'escalade image.png

Télégramme descendant image.png

Le SDK affiche `` {'data': value} '' à l'écran, mais vous pouvez voir que l'équivalent des données envoyées et reçues par curl est en fait retourné.

Avec ce qui précède, il a été confirmé que le format de message de l'exemple curl précédent est équivalent à la méthode d'appel du SDK.

Ensuite, essayez avec l'authentification Firebase

Désormais, à partir de ce moment, nous parlerons progressivement d'authentification.

Ensuite, essayez d'accéder à http: // localhost: 8080 / # / token_auth. Cet écran est [conçu pour protéger avec l'authentification Firebase](https://qiita.com/masatomix/items/5316acdb5980289e5cc2#%E7%94%BB%E9%9D%A2%E3%81%AB% E3% 82% A2% E3% 82% AF% E3% 82% BB% E3% 82% B9% E3% 81% 97% E3% 81% 9F% E3% 81% A8% E3% 81% 8D% E3% 81% AB% E3% 83% AD% E3% 82% B0% E3% 82% A4% E3% 83% B3% E6% B8% 88% E3% 81% BF% E3% 81% A7% E3% 81% AA% E3% 81% 84% E5% A0% B4% E5% 90% 88% E3% 81% AF% E3% 83% AD% E3% 82% B0% E3% 82% A4% E3% 83% B3% E7% 94% BB% E9% 9D% A2% E3% 82% 92% E8% A1% A8% E7% A4% BA% E3% 81% 97% E8% AA% 8D% E8% A8% BC% E3% 81% 97% E3% 81% 9F% E3% 82% 89% E8% A9% B2% E5% BD% 93% E7% 94% BB% E9% 9D% A2% E3% 81% B8% E9% 81% B7% E7% A7% BB% E3% 81% 99% E3% 82% 8B), vous serez donc redirigé vers l'écran de connexion. image.png Il existe un lien pour vous connecter avec votre compte Google et votre authentification par ID / Pass. Veuillez donc vous connecter de la manière qui convient à votre environnement Firebase.

Lorsque vous êtes connecté, vous serez redirigé vers le même écran (Token.vue) que celui avec le bouton.

De même, si vous cliquez sur le bouton et appelez la fonction Functions via le SDK, vous obtiendrez le même résultat, mais il y a une différence dans le message entre le moment où Firebase est authentifié et le moment où ce n'est pas le cas. Plus précisément, lorsque Firebase est authentifié, dans l'en-tête de la requête HTTP,

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZTQ2MGZmM2EzZDQ2ZGZlYzcyNGQ4NDg0ZjczNDc2YzEzZTIwY2YiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb....

Et le jeton de support est défini dans l'en-tête d'autorisation. image.png

En d'autres termes, ** j'ai trouvé que lorsque j'appelle la fonction HTTPS du serveur à l'aide du SDK Firebase, le jeton est défini dans l'en-tête d'autorisation lorsque Firebase est authentifié **.

Au fait, ce jeton est

firebase.auth().currentUser.getIdToken().then(token => console.log(`Bearer ${token}`));

La valeur de token.

Exemple de valeur de jeton

Un échantillon de la valeur réelle ressemble à ceci.

eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZTQ2MGZmM2EzZDQ2ZGZlYzcyNGQ4NDg0ZjczNDc2YzEzZTIwY2YiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20veHh4eHh4LXNhbXBsZXMiLCJhdWQiOiJ4eHh4eHgtc2FtcGxlcyIsImF1dGhfdGltZSI6MTU0OTU5MzgxMCwidXNlcl9pZCI6IlVOWHBENDBZNUVZZkt4eHh4eHh4Iiwic3ViIjoiVU5YcEQ0MFk1RVlmS3h4eHh4eHgiLCJpYXQiOjE1NDk1OTM4MTAsImV4cCI6MTU0OTU5NzQxMCwiZW1haWwiOiJob2dlaG9nZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJob2dlaG9nZUBleGFtcGxlLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19Cg.D3ZDm-UPDGRqezz6Q9QKpZtSVovxlWZNt-pyArLlruo1DqfkmaYkXuTjMpxIbB0sCySDAme3ZeenRYxgBrQJhqZiwFx_mTNfjNoQSUVbRjLrSYdtXLBtgy6OvJGsN93UfFfhb2kAeBjDtOPTE6WOWyJ7wDRK0bmkYvYLZ9NMgFsc9-ELfqew7jOVnZTsem3dwkhfQ-_qHJnRD7xkLmEu2CA0yUSbajVwy-rDpC5eRVZVjnnFpgghJckBpQTdxXesM58aRF5uiSLsIi6KYimDyqV_cQL_oAojW0fR-X-Q0GqD4FYsGmk1hMy-n5ClOUmCKvHLcN6tAWQKScdvYAx3cA

Donc, si vous regardez de près, il est divisé en trois par des points.

Et quand j'essaye de décoder en base64 chacun des deux premiers à l'endroit séparé par des points comme suit, ...

(Seulement ici frappe avec Linux. Cela peut être légèrement différent des options)


$ echo eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZTQ2MGZmM2EzZDQ2ZGZlYzcyNGQ4NDg0ZjczNDc2YzEzZTIwY2YiLCJ0eXAiOiJKV1QifQ | base64 -d | jq
{
  "alg": "RS256",
  "kid": "b2e460ff3a3d46dfec724d8484f73476c13e20cf",
  "typ": "JWT"
}

D'une manière ou d'une autre, JWT et l'algorithme de clé RS256 sont écrits. Et voici le deuxième.

(Seulement ici frappe avec Linux. Cela peut être légèrement différent des options)


$ echo eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20veHh4eHh4LXNhbXBsZXMiLCJhdWQiOiJ4eHh4eHgtc2FtcGxlcyIsImF1dGhfdGltZSI6MTU0OTU5MzgxMCwidXNlcl9pZCI6IlVOWHBENDBZNUVZZkt4eHh4eHh4Iiwic3ViIjoiVU5YcEQ0MFk1RVlmS3h4eHh4eHgiLCJpYXQiOjE1NDk1OTM4MTAsImV4cCI6MTU0OTU5NzQxMCwiZW1haWwiOiJob2dlaG9nZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJob2dlaG9nZUBleGFtcGxlLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19Cg | base64 -d | jq
{
  "iss": "https://securetoken.google.com/xxxxxx-samples",
  "aud": "xxxxxx-samples",
  "auth_time": 1549593810,
  "user_id": "UNXpD40Y5EYfKxxxxxxx",
  "sub": "UNXpD40Y5EYfKxxxxxxx",
  "iat": 1549593810,
  "exp": 1549597410,
  "email": "[email protected]",
  "email_verified": false,
  "firebase": {
    "identities": {
      "email": [
        "[email protected]"
      ]
    },
    "sign_in_provider": "password"
  }
}

Vous pouvez voir que certains types d'informations d'authentification sont inclus. Le troisième bloc concerne les données de signature, mais je l'écrirai plus tard, alors allons-y. ..

functions.https.onCall vérifiera le jeton

À propos, lorsque la fonction enregistrée avec const echo_onCall = functions.https.onCall ((data, context) ... '' est appelée via le SDK, si Firebase est authentifié, une requête avec un jeton Bearer est envoyée. Et il s'avère que cette fonction `ʻonCall le fait, mais quand elle reçoit une requête avec un jeton Bearer, elle semble également vérifier automatiquement ce jeton.

En fait, lorsque je réécris le jeton de manière appropriée et que je le demande avec curl, ...

$ curl -X POST -H "Content-Type:application/json" \
   --data-binary @post_data.json \
   -H 'Authorization:Porteur adapté' \
   https://us-central1-xxxxx-xxxxxxx..cloudfunctions.net/echo_onCall  | jq

  {
  "error": {
    "status": "UNAUTHENTICATED",
    "message": "Unauthenticated"
  }
}

Lorsque j'ai ajouté l'en-tête d'autorisation, j'ai trouvé qu'il vérifie la validité du jeton. Pour le moment, demandons même le bon jeton.

$ curl -X POST -H "Content-Type:application/json" \
   --data-binary @post_data.json \
   -H 'Authorization:Bearer Le bon gars qui vient de rentrer' \
   https://us-central1-xxxxx-xxxxxxx..cloudfunctions.net/echo_onCall

{"result":{"id":"001","name":"Bonjour","isDone":true}}
$

Si vous attachez le bon jeton, vous obtiendrez le même résultat que si vous n'aviez pas l'en-tête d'autorisation.

functions.https.onRequest ne vérifie pas le jeton

Vient ensuite `` functions.https.onRequest '', mais c'est

$ curl -X POST  -H "Content-Type:application/json"   \
 -H 'Authorization:Porteur adapté' \
 --data-binary  @request.json \
 https://us-central1-xxxxx-xxxxxxx..cloudfunctions.net/echo


{
  "id": "001",
  "name": "Bonjour",
  "isDone": true
}
$ (J'étais généralement en écho..)

Même si j'ai défini un jeton approprié, j'ai pu obtenir la valeur. Il s'avère que `` functions.https.onRequest '' ne vérifie pas automatiquement l'en-tête d'autorisation.

Une fois résumé

D'après ce qui précède, il semble qu'il puisse être organisé comme suit.

REST créé avec functions.https.onCall

Cela vérifiera automatiquement la validité du jeton de porteur dans l'en-tête d'autorisation, donc

firebase.auth().currentUser.getIdToken().then(token => console.log(token));

Si vous obtenez le jeton avec, définissez-le dans l'en-tête Authorization et lancez-le, vous pouvez rendre la fonction utilisée uniquement par l'utilisateur qui a authentifié Firebase (bien qu'il soit nécessaire de le traiter comme une erreur si l'en-tête Authorization n'est pas attaché en premier lieu). Cependant, les spécifications de message de demande et de réponse sont fixées dans une certaine mesure.

Il semble que cela puisse être organisé. Surtout s'il est censé être appelé à partir du SDK Firebase, le SDK définit automatiquement le jeton dans l'en-tête Authorization, ce qui est plus simple et plus facile.

REST créé par functions.https.onRequest

Cela ne vérifie pas automatiquement la validité du token Bearer dans l'en-tête Authorization, donc d'abord du client (application WEB ou curl)

firebase.auth().currentUser.getIdToken().then(token => console.log(token));

Obtenez le jeton avec et définissez-le dans l'en-tête d'autorisation afin qu'il puisse être connecté. Et la fonction côté serveur ** interroge Firebase pour la validité du jeton **, il semble donc que seuls les utilisateurs disposant de l'authentification Firebase puissent utiliser la fonction. (Si l'en-tête d'autorisation n'est pas joint en premier lieu, vous devez faire une erreur). Et les spécifications de message de demande et de réponse sont également facultatives.

Il semble que cela puisse être organisé. Cela semble être plus polyvalent lors de la fourniture d'un serveur normalement REST pour les utilisateurs authentifiés Firebase.

Une fonction enregistrée dans Functions qui demande à Firebase la validité du jeton.

Maintenant, le reste est de savoir comment interroger Firebase pour la validité du jeton avec la fonction functions.https.onRequest côté serveur. Par exemple, définissez une fonction pour obtenir la valeur de l'en-tête Authorization de la requête comme indiqué ci-dessous.

index.ts(getIdToken)


function getIdToken(request, response) {
  if (!request.headers.authorization) {
    throw new Error('L'en-tête d'autorisation n'existe pas.')
  }
  const match = request.headers.authorization.match(/^Bearer (.*)$/)
  if (match) {
    const idToken = match[1]
    return idToken
  }
  throw new Error(
    'Impossible d'obtenir le jeton du porteur depuis l'en-tête d'autorisation.',
  )
}

Appelez la fonction avec functions.https.onRequest '' pour obtenir le jeton ID, et utilisez la méthode verifyIdToken '' fournie pour vérifier la validité du jeton ID.

javascript:index.ts(functions.https.onRequest)


import * as corsLib from 'cors'
const cors = corsLib()

export const echo = functions.https.onRequest((request, response) => {
  return cors(request, response, async () => {
    const task = request.body
    console.log(JSON.stringify(task))

    try {
      const idToken = getIdToken(request, response) //Vérifiez si vous pouvez obtenir des jetons de porteur
      const decodedToken = await admin.auth().verifyIdToken(idToken)

      console.log(decodedToken.uid) //UID utilisateur sur l'authentification Firebase
      response.send(JSON.stringify(task))
    } catch (error) {
      console.log(error.message)
      response.status(401).send(error.message)
    }
  })
})

Déployez-le dans Functions, appelez la fonction à partir de curl, etc. avec différentes valeurs du jeton ID et vérifiez l'opération. Vous pouvez voir que les données JSON sont renvoyées uniquement lorsque le jeton d'ID sur le porteur dans l'en-tête d'autorisation est la valeur correcte.

Quel est ce jeton en premier lieu? Et à propos de la falsification et du cryptage

J'ai montré la valeur de l'exemple de jeton d'identification plus tôt, mais ce jeton d'identification est souvent vu dans le monde de l'infrastructure d'authentification telle qu'OpenID Connect JWT (JSON Web Tokens) Est utilisé. Je laisserai les détails à quelqu'un qui le connaît :-) JWT consiste à envoyer des données JSON au réseau.

Partie d'en-tête.Payload (corps JSON) .Signature (signature)

C'est un mécanisme pour envoyer dans un format divisé par des points (chaque partie est encodée en Base64). La signature est les données qui ont signé la section En-tête et la section Charge utile par la méthode de signature décrite dans la section En-tête (généralement, elle est souvent signée avec la clé privée du créateur), et par conséquent, l'en-tête est signé avec la clé publique du créateur. Vous pouvez vérifier la falsification de / Payload.

En d'autres termes, par exemple, je le ferai ensuite, mais ** Si vous souhaitez utiliser l'authentification Firebase comme base d'authentification / d'autorisation de votre propre serveur REST ** En recevant ce jeton d'identification, le système Firebase qui est l'émetteur du jeton d'identification Il est possible d'obtenir la clé publique et de vérifier la validité des données JWT.

En outre, l'ID utilisateur Firebase peut finalement être obtenu à partir du jeton d'identification, mais comme la falsification peut être détectée, elle empêche également l'usurpation d'identité et la falsification des données.

Au fait, dans les données JSON de la section Payload

{
  "iss": "https://securetoken.google.com/xxxxxx-samples",
  "aud": "xxxxxx-samples",
  "auth_time": 1549593810,
  "user_id": "UNXpD40Y5EYfKxxxxxxx",
  "sub": "UNXpD40Y5EYfKxxxxxxx", //ID utilisateur Firebase
  "iat": 1549593810,
  "exp": 1549597410,  //  <- Fri Feb 08 12:43:30 JST 2019 date d'expiration
  "email": "[email protected]",
  "email_verified": false,
  "firebase": {
    "identities": {
      "email": [
        "[email protected]"
      ]
    },
    "sign_in_provider": "password"
  }
}

Puisque la date d'expiration est indiquée comme ceci, vous pouvez vérifier s'il s'agit d'une demande valide et authentifiée en vérifiant cette date et cette heure.

Cependant, JWT n'est que des données encodées en Base64, elles ne sont donc pas cryptées. Par conséquent, si vous ne voulez pas voir le contenu, vous devez passer SSL correctement. Eh bien, ce n'est pas grave car REST est créé avec SSL. ..

Je souhaite vérifier la validité des jetons depuis mon propre serveur REST, pas depuis Functions

Eh bien, j'ai dit que je le ferais ensuite, mais ** J'utiliserai l'authentification Firebase comme plate-forme d'authentification / d'autorisation pour mon propre serveur REST . Plus précisément, j'ai vérifié la validité du jeton d'identification à partir de la fonction enregistrée dans les Fonctions de Firebase plus tôt, mais cette fois j'ai vérifié la validité du jeton d'identification du serveur REST que j'ai construit par moi-même ( En vérifiant la signature) La vérification de la falsification des données et la confirmation de la date d'expiration **) seront effectuées.

Au fait, à proprement parler

Ce site explique en détail que vous devriez vérifier cela.

Créez votre propre serveur REST

C'est pourquoi nous allons construire un serveur REST dans un environnement qui n'a rien à voir avec Functions ou Firebase.

Nous avons déjà créé et validé un serveur Java qui démarre à http: // localhost: 8081 / echo, alors supprimons-le et construisons un environnement.

Ah ... Vous devez installer Apache Maven pour construire et démarrer le serveur, mais j'omettrai la procédure d'installation de Maven. .. ..

$ git clone --branch 0.0.1 https://github.com/masatomix/spring-boot-sample-tomcat.git 
$ cd spring-boot-sample-tomcat
$ mvn eclipse:clean eclipse:eclipse
$ mvn spring-boot:run

Devrait commencer par.

Appel de Curl

Ce serveur REST provient de curl

$ cat post_data.json
{
    "id": "001",
    "name": "Bonjour",
    "isDone": true
}:

$ curl -X POST -H "Content-Type:application/json" \
  --data-binary @post_data.json \
  -H 'Authorization: Bearer eyJhbGciOiJSUzI1xxxxxxxxxx'\
  http://localhost:8081/echo

Vous pouvez l'appeler avec. Comme d'habitude, essayez de récupérer le jeton d'identification à l'aide des outils de développement de votre navigateur.

image.png

Ajout de l'écran WEB pour appeler REST

Pour appeler depuis l'écran WEB, si vous remplacez le Token.vue du projet créé avec Vue.js par le suivant, un autre bouton apparaîtra. Vous pouvez envoyer une demande REST en appuyant sur ce bouton, veuillez donc le vérifier. Ce code est un exemple de code qui transmet un jeton d'ID à l'en-tête d'autorisation lors d'un appel REST.

Token.vue


<template>
  <main class="container">
    <div>Appel HTTP!</div>
    <button class="btn btn-primary" @click="checkToken">Appel HTTP!</button>

    <hr>
    <div>Appel HTTP!</div>
    <button class="btn btn-primary" @click="checkToken_without_sdk">Appel HTTP(Propre serveur)!</button>
  </main>
</template>

<script>
import firebase from "firebase";
import axios from "axios";

export default {
  name: "Token",
  methods: {
    checkToken() {
      if (firebase.auth().currentUser) {
        firebase.auth().currentUser.getIdToken()
          .then(token => console.log(token));
      } else {
        console.log("currentUser is null");
      }

      const value = {
        id: "001",
        name: "Bonjour",
        isDone: true
      };
      //Appel HTTP
      const echo_onCall = firebase.functions().httpsCallable("echo_onCall");
      echo_onCall(value).then(result => alert(JSON.stringify(result)));
    },

    checkToken_without_sdk() {
      if (firebase.auth().currentUser) {
        firebase.auth().currentUser.getIdToken()
          .then(token => {
            console.log(token);

            const value = {
              id: "001",
              name: "Bonjour",
              isDone: true
            };
            const config = {
              url: "http://localhost:8081/echo",
              method: "POST",
              headers: {
                "Content-type": "application/json",
                Authorization: `Bearer ${token}`
              },
              data: value,
              json: true
            };
            axios(config)
              .then(response => alert(JSON.stringify(response.data)))
              .catch(error => alert(error.message));
          });
      } else {
        console.log("currentUser is null");
      }
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

image.png

Le code qui possède le serveur REST vérifie la validité des jetons d'identification

Enfin, le code Java du serveur REST. La validité du jeton d'identification est vérifiée à l'aide de la bibliothèque JWT appelée Nimbus JOSE + JWT.

SampleController.java


package nu.mine.kino.web;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import nu.mine.kino.service.Hello;

@Controller
@RequestMapping("/echo")
public class SampleController {
    private static final Pattern CHALLENGE_PATTERN = Pattern
            .compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE);

    @ResponseBody
    @CrossOrigin
    @RequestMapping(produces = "application/json; charset=utf-8", method = RequestMethod.POST)
    public Hello helloWorld(
            @RequestHeader(value = "Authorization", required = true) String authorization,
            @RequestBody Hello hello) throws UNAUTHORIZED_Exception {

        Matcher matcher = CHALLENGE_PATTERN.matcher(authorization);
        if (matcher.matches()) {
            String id_token = matcher.group(1);
            if (JWTUtils.checkIdToken(id_token)) {
                return hello;
            } else {
                throw new UNAUTHORIZED_Exception("ID Token is invalid.");
            }
        }
        throw new UNAUTHORIZED_Exception(
                "Impossible d'obtenir le jeton du porteur depuis l'en-tête d'autorisation.");
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    class UNAUTHORIZED_Exception extends Exception {
        private static final long serialVersionUID = -6715447914841144335L;

        public UNAUTHORIZED_Exception(String message) {
            super(message);
        }
    }

    // private void snippet() throws IOException, FirebaseAuthException {
    // String id_token = "";
    // ClassLoader loader = Thread.currentThread().getContextClassLoader();
    // //Mettez-le dans les ressources
    // InputStream serviceAccount = loader.getResourceAsStream(
    // "xxxxxxxx-firebase-adminsdk-xxxxxxxxx.json");
    // FirebaseOptions options = new FirebaseOptions.Builder()
    // .setCredentials(GoogleCredentials.fromStream(serviceAccount))
    // .setDatabaseUrl("https://xxxxxxxx.firebaseio.com").build();
    // FirebaseApp.initializeApp(options);
    //
    // FirebaseToken decodedToken = FirebaseAuth.getInstance()
    // .verifyIdToken(id_token);
    // String uid = decodedToken.getUid();
    //
    // }
}

Le code appelé par `ʻif (JWTUtils.checkIdToken (id_token)) ..`` est le suivant.

JWTUtils.java


package nu.mine.kino.web;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.X509CertUtils;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import lombok.extern.slf4j.Slf4j;

/**
 * @author Masatomi KINO
 * @version $Revision$
 */
@Slf4j
public class JWTUtils {
    public static boolean checkIdToken(String id_token) {
        try {
            SignedJWT decodeObject = SignedJWT.parse(id_token);
            log.debug("Header : " + decodeObject.getHeader());
            log.debug("Payload: " + decodeObject.getPayload());
            log.debug("Sign   : " + decodeObject.getSignature());

            JWSAlgorithm algorithm = decodeObject.getHeader().getAlgorithm();
            JWTClaimsSet set = decodeObject.getJWTClaimsSet();
            log.debug("Algorithm: {}", algorithm.getName());
            log.debug("ExpirationTime(date d'expiration): {}", set.getExpirationTime());
            log.debug("IssueTime(Date et heure d'émission): {}", set.getIssueTime());
            log.debug("Subject(key): {}", set.getSubject());
            log.debug("Issuer(Éditeur): {}", set.getIssuer());
            log.debug("Audience: {}", set.getAudience());
            log.debug("Nonce: {}", set.getClaim("nonce"));
            log.debug("auth_time(Date et heure d'authentification): {}", set.getDateClaim("auth_time"));
            log.debug("Algorithme clé({})", algorithm.getName());
            boolean isExpired = new Date().after(set.getExpirationTime());
            log.debug("now after ExpirationTime?: {}", isExpired);
            if (isExpired) {
                log.warn("Il a expiré.");
                return false;
            }
            String jwks_uri = "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]";
            return checkRSSignature(decodeObject, jwks_uri);

        } catch (ParseException e) {
            log.warn("Impossible d'obtenir la clé publique du serveur.{}", e.getMessage());
        } catch (IOException e) {
            log.warn("Impossible d'obtenir la clé publique du serveur.{}", e.getMessage());
        } catch (JOSEException e) {
            log.warn("Vérifiez que le traitement a échoué.{}", e.getMessage());
        }
        return false;
    }

    private static boolean checkRSSignature(SignedJWT decodeObject,
            String jwks_uri) throws JOSEException, IOException, ParseException {
        //Obtenez le KeyID de l'en-tête
        String keyID = decodeObject.getHeader().getKeyID();
        log.debug("KeyID: {}", keyID);

        Map<String, Object> resource = getResource(jwks_uri);
        String object = resource.get(keyID).toString();
        X509Certificate cert = X509CertUtils.parse(object);
        RSAKey rsaKey = RSAKey.parse(cert);

        JWSVerifier verifier = new RSASSAVerifier(rsaKey);
        boolean verify = decodeObject.verify(verifier);
        log.debug("valid?: {}", verify);
        return verify;
    }

    private static Map<String, Object> getResource(String target)
            throws IOException {
        Client client = ClientBuilder.newClient();
        Response restResponse = client.target(target)
                .request(MediaType.APPLICATION_JSON_TYPE).get();
        String result = restResponse.readEntity(String.class);
        log.debug(result);
        return json2Map(result);
    }

    private static Map<String, Object> json2Map(String result)
            throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(result,
                new TypeReference<Map<String, Object>>() {
                });
    }
}

Le code Java est long et humoristique, mais c'est presque comme ça. ..

--SampleController # helloWorld est appelé lors de l'accès à http: // localhost: 8081 / echo

En faisant cela, le contrôle de validité du jeton d'identification est terminé!

Cette fois, j'ai vérifié moi-même le jeton d'identification en Java, mais en fait, le langage principal est le SDK fourni par Firebase.

Avec le SDK Java, j'ai pu l'initialiser et l'utiliser sur le serveur REST (j'ai également laissé le code en commentaire), mais "Créer une clé privée sur l'écran du compte de service" ou "Compte de service J'ai besoin d'un fichier de configuration avec des informations d'authentification », et ainsi de suite, je vais donc sauter la procédure.

Ah, j'étais fatigué.

Résumé

Les informations d'authentification lors de l'authentification avec Firebase étaient dans un format appelé jeton d'identification, qui est une spécification appelée JWT qui peut être vérifiée pour la falsification. Ainsi, lors de la protection de l'API publiée sur le serveur REST avec authentification, quelle que soit la méthode d'implémentation (fonctions Firebase, propre serveur REST, etc.), faites envoyer le jeton d'ID au moment de l'appel de l'API. J'ai donc découvert que je pouvais vérifier si j'étais un utilisateur légitime connecté à Firebase.

De plus, comme le jeton d'ID contient l'ID utilisateur (la sous-propriété de JWT), qui est la valeur clé pour l'authentification Firebase, utilisez cette valeur après avoir vérifié le jeton d'ID pour le lier aux données utilisateur dans votre propre base de données. Il semble que vous pouvez le faire.

c'est tout. Merci pour votre travail. .. ..

Liens connexes (source)

La source du projet à déployer sur Functions après l'ajout du code.

$ git clone --branch restsamples001 https://github.com/masatomix/fb_function_samples.git
$ cd fb_function_samples/functions/
$ npm install
$ cd ../

$ firebase use --add   //Sélectionnez votre projet sur
$ firebase deploy --only functions

Source de l'application WEB créée avec Vue.js une fois l'ajout de code terminé

$ git clone --branch idToken002 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.Réécrire js selon vos propres paramètres

$ npm run dev

Exemple de source du propre serveur REST créé en Java

$ git clone --branch 0.0.1 https://github.com/masatomix/spring-boot-sample-tomcat.git 
$ cd spring-boot-sample-tomcat
$ mvn eclipse:clean eclipse:eclipse
$ mvn spring-boot:run

Liens connexes

Recommended Posts

Protégez les API REST créées avec Cloud Functions avec l'authentification Firebase. Et appliquez l'authentification Firebase à l'API de votre propre serveur REST.
Appliquez votre propre domaine à Rails of VPS et rendez-le SSL (https) (CentOS 8.2 / Nginx)
Ce à quoi j'étais accro avec l'API REST Redmine
Lisez les données de Shizuoka Prefecture Point Cloud DB avec Java et essayez de détecter la hauteur de l'arbre.
J'ai essayé de créer un environnement de serveur UML Plant avec Docker
J'ai essayé de vérifier le fonctionnement du serveur gRPC avec grpcurl