[JAVA] Protect REST APIs built with Cloud Functions with Firebase authentication. And apply Firebase authentication to the API of your own REST server.

Last time called Cloud Functions for Firebase, a logic placed in a serverless environment, via HTTPS.

Now, regarding the HTTPS call, the functions.https.onRequest: used at that time.

index.ts


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

Apart from that, there seems to be a method called functions.https.onCall, and this time I will use that method. And with that as a starting point, let's think about how to protect the REST API built on Functions with the authentication function of Firebase.

By the way, I'd like to incorporate Firebase authentication into the REST server I built myself.

TL;DR

If you create a REST service with functions.https.onRequest, authentication check etc. will not be performed automatically. Therefore, it is necessary to create it, but by putting the Firebase ID token in the Authorization header, it can be used only by Firebase authenticated users.

In addition, since the ID token is sent in JWT format that can be tampered with, the function side can check the validity by inquiring Firebase, and the user ID numbered by Firebase can be retrieved.

You can have Firebase check the validity of the ID token by inserting the SDK for the REST server you created yourself, and even if you can not insert the SDK, check the validity with the so-called JWT token library etc. Is possible. I tried both SDK and myself, but I thought that it would be easier to prepare a library and check it by myself if the environment setting is rather troublesome to put in the SDK.

It looks like this in the figure.

image.png

image.png

I haven't deployed the WEB application to the server this time, but I wrote it assuming that it will be deployed to Firebase Hosting. Also, the necessity of CORS (Cross-Origin Resource Sharing) support is not properly described in this article, but onCall seems to be CORS compatible.

First try with Firebase authentication pear

Now let's use functions.https.onCall. I want to add functionality to my previous project, so I'll get my previous project from Github.

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

$ firebase use --add   //Select your project at

Now let's build by adding the following to 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
})

Build:

$ firebase deploy --only functions

Call with Curl from the command line

First of all, review. The original const hello = functions.https.onRequest ((req, res) ... could be called by the following method.

$ cat request.json
{
  "id": "001",
  "name": "Hello",
  "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": "Hello",
  "isDone": true
}
$ (Usually echoed)

By the way, const echo_onCall = functions.https.onCall ((data, context) ... For this, it seems that the format of the call is decided to some extent.

Detail is

--https.onCall protocol specification --Call a function from the app

You can call it from curl as follows.

$ cat post_data.json
{
  "data": { //Wrap it in data,
    "id": "001",
    "name": "Hello",
    "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": { //Wrapped in result and returned
    "id": "001",
    "name": "Hello",
    "isDone": true
  }
}

The newly registered function simply returns the argument data, but of the POSTed data, the part wrapped in the data property is extracted, set in the result property, and returned.

Well, it's hard to understand, but I understand how it works orz. ..

Build a Vue.js project and try calling it via the SDK

By the way, functions.https.onCall ((data, context) ..., but when calling this function from the WEB, it seems that it is supposed to be called from the Firebase SDK (Firebase JavaScript SDK etc.) Right.

So, next, I will call it via SDK using the application built with Vue.js.

Notes when using the main functions of Vue.js roughly (Firebase authentication / authorization) ToDo with Firebase authentication I have an app, so I'll use it. Also, please enable Authentication etc. on the Firebase side according to this article. I'll use authentication later.

Click here for how to build a Vue.js app.

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

src/firebaseConfig.Rewrite js to your own settings

$ npm run dev

Now that the server has started, try accessing http: // localhost: 8080 / # / token /. image.png

Since this screen is not protected by Firebase authentication, the WEB screen with the button is displayed. Try clicking that button. image.png

Some data has been returned.

By the way, this source is as follows.

Token.vue


<template>
  <main class="container">
    <div>HTTP call!</div>
    <button class="btn btn-primary" @click="checkToken">HTTP call!</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: "Hello",
        isDone: true
      };
      //HTTP call
      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>
  

The echo_onCall function registered in Firebase Functions was supposed to just return the argument, but when the button is pressed, the SDK function const echo_onCall = firebase.functions (). httpsCallable ("echo_onCall") , ʻecho_onCall (value) `It seems that the value passed is wrapped in the data property in the callback and returned.

Let's look at the up and down telegrams with a browser.

Climbing telegram image.png

Down telegram image.png

The SDK displays {'data': value} on the screen, but you can see that the data equivalent to the data sent and received by curl is actually returned.

With the above, it was confirmed that the telegram format of the previous curl example is equivalent to how to call the SDK.

Next, try with Firebase authentication

Now, from this point on, we will gradually talk about authentication.

Next, try accessing http: // localhost: 8080 / # / token_auth. This screen is [built to protect with Firebase authentication](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), so you will be redirected to the login screen. image.png We have placed a link to log in with your Google account and ID / Pass authentication, so please log in as appropriate in the way that suits your Firebase environment.

When you are logged in, you will be taken to the same screen (Token.vue) as the one with the button.

If you also click the button and call the Functions function via the SDK, you will get the same result, but there is a difference in the telegram between Firebase authenticated and unauthenticated. Specifically, when Firebase is authenticated, in the HTTP request header,

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImIyZTQ2MGZmM2EzZDQ2ZGZlYzcyNGQ4NDg0ZjczNDc2YzEzZTIwY2YiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb....

And the Bearer token is set in the Authorization header. image.png

In other words, it turns out that if you use the Firebase SDK to call the HTTPS Function of the server, the token will be set in the Authorization header when you are authenticating with Firebase **.

By the way, this token is

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

The value of token.

Token value sample

A sample of the actual value looks like this.

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

So, if you look closely, it is divided into three by dots.

And when I try to base64 decode each of the first two at the place separated by dots as follows, ...

(Only here is hitting with linux. It may be slightly different from the options)


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

Somehow JWT and the key algorithm RS256 are written. And here is the second one.

(Only here is hitting with linux. It may be slightly different from the 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"
  }
}

You can see that some authentication information is included. The third block is signature data, but I will write it later, so let's go next. ..

functions.https.onCall will verify the token

By the way, when the function registered with const echo_onCall = functions.https.onCall ((data, context) ... is called via SDK, if Firebase authenticated, send a request with Bearer token. And it turns out that this `ʻonCall`` function does, but when it receives a request with a Bearer token, it seems to automatically verify that token as well.

In fact, when I rewrite the token appropriately and request it with curl, ...

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

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

When I added the Authorization header, I found that it checks the validity of the token. For the time being, let's request even the correct token.

$ curl -X POST -H "Content-Type:application/json" \
   --data-binary @post_data.json \
   -H 'Authorization:Bearer The right guy who just came back' \
   https://us-central1-xxxxx-xxxxxxx..cloudfunctions.net/echo_onCall

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

If you attach the correct token, you will get the same result as if you did not have the Authorization header.

functions.https.onRequest does not verify token

Next is functions.https.onRequest, which is

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


{
  "id": "001",
  "name": "Hello",
  "isDone": true
}
$ (I was usually echoed..)

Even if I set an appropriate token, I was able to get the value. It turns out that functions.https.onRequest doesn't automatically check the Authorization header.

Once summarized

From the above, it seems that it can be organized as follows.

REST created with functions.https.onCall

This will automatically check the validity of the Bearer token in the Authorization header, so

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

If you get the token with, set it in the Authorization header and throw it, you can make the function used only by the user who authenticates Firebase (although it is necessary to process it as an error if the Authorization header is not attached in the first place). However, the telegram specifications of request and response are fixed to some extent.

It seems that it can be organized. Especially if it is assumed that it will be called from the Firebase SDK, the SDK will automatically set the token in the Authorization header, which is simpler and easier.

REST created by functions.https.onRequest

Here, the validity of the Bearer token in the Authorization header is not checked automatically, so first from the client (WEB application or curl)

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

Get the token with and set it in the Authorization header so that it can be connected. And the server-side function ** queries Firebase for the validity of the token **, so it seems that only Firebase-authenticated users can use the function. (If the Authorization header is not attached in the first place, you have to make an error). And the telegram specifications of request and response are also optional.

It seems that it can be organized. If you usually provide a REST server for Firebase authenticated users, this seems to be more versatile.

A function registered in Functions that asks Firebase for the validity of the token.

Now, the rest is how to query Firebase for the validity of the token with the functions.https.onRequest function on the server side. For example, define a function to get the value of the authorization header of the request as shown below.

index.ts(getIdToken)


function getIdToken(request, response) {
  if (!request.headers.authorization) {
    throw new Error('Authorization header does not exist.')
  }
  const match = request.headers.authorization.match(/^Bearer (.*)$/)
  if (match) {
    const idToken = match[1]
    return idToken
  }
  throw new Error(
    'Could not get Bearer token from Authorization header.',
  )
}

Call the function with functions.https.onRequest to get the ID token, and use the provided verifyIdToken method to check the validity of the ID token.

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) //Check if you can get Bearer tokens
      const decodedToken = await admin.auth().verifyIdToken(idToken)

      console.log(decodedToken.uid) //User UID on Firebase Authentication
      response.send(JSON.stringify(task))
    } catch (error) {
      console.log(error.message)
      response.status(401).send(error.message)
    }
  })
})

Deploy it to Functions, call the function from curl etc. with various values of ID token, and check the operation. You can see that the JSON data is returned only when the ID token on the Bearer in the Authorization header has the correct value.

-** 2020/02/01 postscript - I changed the getIdToken function because it's smarter to throw an exception than to return a screen with response. The `ʻecho`` function supports CORS because it is necessary to consider CORS when making a request from a web browser. - Added on 2020/02/01 Above **-

What is this token in the first place? And about tampering and encryption

I showed the value of the ID token sample earlier, but this ID token is often seen in the world of authentication infrastructure such as OpenID Connect JWT (JSON Web Tokens) Is used. I'll leave the details to someone who is familiar with it :-) JWT is when sending JSON data to the network.

Header part. Payload (JSON body) .Signature (signature)

It is a mechanism to send in a format divided by dots (each part is Base64 encoded). The signature is the data that signed the Header part and the Payload part by the signature method described in the Header part (usually, it is often signed with the private key of the creator), and therefore the Header is made with the public key of the creator. You can check the tampering of / Payload.

That is, for example, I will do it next, but ** If you want to use Firebase authentication as the authentication / authorization base of your own REST server ** By receiving this ID token, the Firebase system that is the issuer of the ID token It is possible to get the public key of and check the validity of JWT data.

In addition, the Firebase user ID can be finally obtained from the ID token, but since the tampering can be detected, it also prevents spoofing and data tampering.

By the way, in the JSON data of the Payload part

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

Since the expiration date is stated like this, you can check whether the request is authenticated and legitimate by checking this date and time.

However, JWT is just Base64-encoded data, so it is not encrypted. Therefore, if you do not want to see the contents, you need to pass SSL properly. Well, it's okay because REST is created with SSL. ..

I want to check the validity of tokens from my own REST server, not from Functions

Well, I said I'd do it next, but ** I'll use Firebase authentication as the authentication / authorization platform for my own REST server . Specifically, I checked the validity of the ID token from the function registered in Firebase Functions earlier, but this time I checked the validity of the ID token from the REST server I built by myself ( By verification of signature) We will verify data tampering and confirm the expiration date **).

By the way, strictly speaking

--Verify ID Token

This site explains in detail that you should check this out.

Build your own REST server

That's why we will build a REST server in an environment that has nothing to do with Functions or Firebase.

We have already created and committed a Java server that starts up at http: // localhost: 8081 / echo, so let's drop it and build an environment.

Ah ... You need to install Apache Maven to build and start the server, but I will omit the Maven setup procedure. .. ..

$ 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

Should start with.

Call from Curl

This REST server is from curl

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

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

You can call it with. As usual, try to retrieve the ID token using your browser's development tools.

image.png

Addition of WEB screen to call REST

For calling from the WEB screen, if you replace the Token.vue of the project created with Vue.js with the following, another button will appear. You can send a REST request by pressing that button, so please check it. This code is a sample code that passes an ID token to the Authorization header when making a REST call.

Token.vue


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

    <hr>
    <div>HTTP call!</div>
    <button class="btn btn-primary" @click="checkToken_without_sdk">HTTP call(Own server)!</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: "Hello",
        isDone: true
      };
      //HTTP call
      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: "Hello",
              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

Code that own REST server checks the validity of ID token

Finally, the Java code for the REST server. The validity of the ID token is checked using the JWT library called 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(
                "Could not get Bearer token from Authorization header.");
    }

    @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();
    // //Put it in resources
    // 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();
    //
    // }
}

The code called by `ʻif (JWTUtils.checkIdToken (id_token)) ..`` is as follows.

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(expiration date): {}", set.getExpirationTime());
            log.debug("IssueTime(Issue date and time): {}", set.getIssueTime());
            log.debug("Subject(key): {}", set.getSubject());
            log.debug("Issuer(Publisher): {}", set.getIssuer());
            log.debug("Audience: {}", set.getAudience());
            log.debug("Nonce: {}", set.getClaim("nonce"));
            log.debug("auth_time(Authentication date and time): {}", set.getDateClaim("auth_time"));
            log.debug("Key algorithm({})", algorithm.getName());
            boolean isExpired = new Date().after(set.getExpirationTime());
            log.debug("now after ExpirationTime?: {}", isExpired);
            if (isExpired) {
                log.warn("It has expired.");
                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("Failed to get the public key of the server.{}", e.getMessage());
        } catch (IOException e) {
            log.warn("Failed to get the public key of the server.{}", e.getMessage());
        } catch (JOSEException e) {
            log.warn("Verify processing has failed.{}", e.getMessage());
        }
        return false;
    }

    private static boolean checkRSSignature(SignedJWT decodeObject,
            String jwks_uri) throws JOSEException, IOException, ParseException {
        //Get the KeyID from the Header
        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>>() {
                });
    }
}

The Java code is long and humorous, but what I'm doing is something like this. ..

--SampleController # helloWorld is called when accessing http: // localhost: 8081 / echo --Extract the ID token (id_token) from the Bearer part of the Authorization header --If JWTUtils.checkIdToken (id_token) is called and OK, the subsequent processing is executed as a legitimate request. --The processing of JWTUtils.checkIdToken (id_token) --Create a SignedJWT object in the Nimbus JOSE + JWT library and retrieve Header / Payload / Sign --Judge the signature algorithm from the information in the Header section (actually, skip it, RS256 Kimeuchi) --Expiration check by looking at ExpirationTime (exp property) in the JSON part of Payload --checkRSSignature method call --Get the Firebase public key (s) from the specified URL (jwks_uri) --By the way, JWK URL (site to get public key) etc. is Check ID token Here It is listed in --Select the public key corresponding to the KeyID in the header and verify the signature of Header / Payload using the RSASSAVerifier class.

By doing this, the validity check of the ID token is completed!

This time, I checked the ID token by myself in Java, but in fact, the main language is the SDK provided by Firebase.

--Add Firebase Admin SDK to server

With the Java SDK, I was able to initialize and use it on the REST server (I also left the code as a comment), but "Create a private key on the service account screen" or "Service account I need a configuration file with authentication information, "and so on, so I'll skip the procedure.

Ah, I was tired.

Summary

The authentication information when authenticating with Firebase was in a format called ID token, which is a specification called JWT that can be checked for tampering. So, when protecting the API published on the REST server with authentication, regardless of the implementation method (Firebase Functions, own REST server, etc.), have the ID token sent at the time of API call. So I found out that I can check if I am a legitimate Firebase logged-in user.

Also, since the ID token contains the user ID (JWT'sub'property) that is the key value for Firebase authentication, use that value after checking the ID token to link it with the user data in your own database. It seems that you can do it.

that's all. Thanks for your work. .. ..

Related links (source)

The source of the project to be deployed to Functions after the code addition is completed.

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

$ firebase use --add   //Select your project at
$ firebase deploy --only functions

Source of WEB application created with Vue.js after code addition is completed

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

src/firebaseConfig.Rewrite js to your own settings

$ npm run dev

Sample source of own REST server created in 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

Related Links

--https.onCall protocol specification --Call a function from the app --Call a function via HTTP request -Try Firebase HTTPS callable function -Summary of findings obtained using Firebase Authentication --Token specifications and precautions JWT specifications for return telegram Is explained carefully. --Verify ID Token -Notes on using the main functions of Vue.js roughly (Firebase authentication / authorization) --Building an authorization server using Authlete and communicating from an OAuth client (Web application) -Get Firebase user information logged in with a browser on the server side -Link Firebase Auth's user authentication function with your own database

Recommended Posts

Protect REST APIs built with Cloud Functions with Firebase authentication. And apply Firebase authentication to the API of your own REST server.
Apply your own domain to Rails of VPS and make it SSL (https) (CentOS 8.2 / Nginx)
What I was addicted to with the Redmine REST API
Read the data of Shizuoka point cloud DB with Java and try to detect the tree height.
I tried to build the environment of PlantUML Server with Docker
I tried to check the operation of gRPC server with grpcurl