Implement Sign In With Google on the backend side

This article is the 24th day article of LinuxClub Advent Calendar 2020. ~~ I was late ... ~~ As the title suggests, it is a story of implementing Sign In With Google on the back end side and getting an email address.

background

I had to implement the function of logging in with a Google account for personal cases, and there was a part that golang.org/x/oauth2 could not do, so I implemented that part myself.

The environment is Go 1.15.5.

The part that could not be done with golang.org/x/oauth2

The details can be found by looking at the library document [^ 1] and the source code, but in golang.org/x/oauth2, the oauth2.Token structure obtained from the library has the following form. We now have:

type Token struct {
	// AccessToken is the token that authorizes and authenticates
	// the requests.
	AccessToken string `json:"access_token"`

	// TokenType is the type of token.
	// The Type method returns either this or "Bearer", the default.
	TokenType string `json:"token_type,omitempty"`

	// RefreshToken is a token that's used by the application
	// (as opposed to the user) to refresh the access token
	// if it expires.
	RefreshToken string `json:"refresh_token,omitempty"`

	// Expiry is the optional expiration time of the access token.
	//
	// If zero, TokenSource implementations will reuse the same
	// token forever and RefreshToken or equivalent
	// mechanisms for that TokenSource will not be used.
	Expiry time.Time `json:"expiry,omitempty"`

	// contains filtered or unexported fields

}

It's extracted directly from the documentation, but you only get four things: AccessToken, TokenType, RefreshToken, and Expiry. Therefore, when it comes to getting an email address, you only get this information. This will increase unnecessary processing such as hitting the API using the access token. This time, let's do various parts. I will write the details about it in the implementation section.

Implementation

The whole code is given on GitHub, so if you don't have enough, please refer to that as well.

The file structure is as follows. Mainly the implementation of callback.go is the main.

.
├── callback.go
├── config.go
├── go.mod
├── go.sum
├── main.go
└── signin.go

In config.go, the information necessary for authentication such as ClientID is stored. Since I don't hit the API this time, the scope is only profile and email. Also, the redirect destination is / callback. Specify the redirect destination in Google Developer Console, etc. I also need it, so please do that as well.

config.go


package main

import (
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

var AuthConfig = &oauth2.Config{
	ClientID:     "<Your ClientID>",
	ClientSecret: "<Your Client Secret>",
	Endpoint:     google.Endpoint,
	Scopes: []string{
		"profile",
		"email",
	},
	RedirectURL: "http://localhost:8080/callback",
}

In signin.go, it is the process up to the redirect to authentication. The generation of the authentication URL is left to golang.org/x/oauth2. Originally, the argument ofAuthConfig.AuthCodeURL ()is a random value for CSRF countermeasures, and it is set to callback.go. This time the value is fixed for simplicity.

Reference: https://developers.google.com/identity/protocols/oauth2/openid-connect#state-param

signin.go


package main

import "net/http"

func handlerSignIn(w http.ResponseWriter, r *http.Request) {
	url := AuthConfig.AuthCodeURL("state") //Originally a random value for CSRF measures
	http.Redirect(w, r, url, 302)
}

The main callback.go gets the email address from the redirect. As I wrote earlier, oauth2.Token does not contain the necessary information. Google's OAuth2 authentication includes an AccessToken as well as a JWT-formatted ID token, which contains the email address. Therefore, in order to get the ID token, you can use Extra ("id_token "). (String) for oauth2.Token obtained byExchange ()to get the ID token. The return value of Extra () is of type interface {}, so it is extracted with . (String). After that, parse the JWT to get the email address. I will omit the explanation of the JWT specification, so please check it by yourself. Since the ID token is Base64 encoded, the payload part is decoded and the email address is obtained. Get it. The processing of that part is written in parseJWT ().

callback.go


package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
)

func handlerCallback(w http.ResponseWriter, r *http.Request) {
	queries := r.URL.Query()

	if queries == nil {
		fmt.Fprintf(w, "Invalid URL.")
		return
	}

	fmt.Println("------ Queries ------")

	for k, v := range queries {
		fmt.Println(k, v)
	}

	code := queries.Get("code")

	token, err := AuthConfig.Exchange(context.Background(), code)

	if err != nil {
		fmt.Fprintf(w, "%s", err)
		return
	}

	idToken := token.Extra("id_token").(string)

	fmt.Println("------ ID Token ------")
	fmt.Println(idToken)

	email, err := parseJWT(idToken)

	if err != nil {
		fmt.Fprintf(w, "%s", err)
		return
	}

	fmt.Fprintf(w, "email: %s", email)
}

type jwtData struct {
	Email string `json:"email"`
}

func parseJWT(token string) (string, error) {
	jwt := strings.Split(token, ".")
	payload := strings.TrimSuffix(jwt[1], "=")
	b, err := base64.RawURLEncoding.DecodeString(payload)

	if err != nil {
		return "", fmt.Errorf("failed decoding base64")
	}

	fmt.Println("------ JWT Data ------")
	fmt.Println(string(b))

	jd := &jwtData{}

	if err := json.Unmarshal(b, jd); err != nil {
		return "", fmt.Errorf("failed unmarshal json data (in parseJWT())")
	}

	return jd.Email, nil
}

Finally, in the execution part main.go, redirect processing is added to/, redirect processing is added to/callback, and it listens on port 8080.

main.go


package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", handlerSignIn)
	http.HandleFunc("/callback", handlerCallback)

	fmt.Println("Listen here: http://localhost:8080")

	log.Fatal(http.ListenAndServe(":8080", nil))
}

If you run it and connect to localhost: 8080, you will be taken to the familiar authentication screen and localhost: 8080/callback will display email: <email address>. The console will show the result of parsing, etc. Is output, so it's easy to check what's going on.

Summary

I got the ID token from Google authentication, parsed it and got the email address. If you actually do it, you need to add processing such as the state for CSRF measures and whether the email address has been authenticated. ..

By the way, is the order a rabbit tomorrow? It's the release date of Volume 9. Please buy it.

Digression

At first, I thought that I couldn't get the ID token at golang.org/x/oauth2, so I wrote the token acquisition part from scratch, but when I read back the document while writing the article, I can do it properly. … Let's read the document properly ...

Recommended Posts

Implement Sign In With Google on the backend side
Drawing tips with matplotlib on the server side
Memo to get the value on the html-javascript side with jupyter
Duplicate the document template prepared in Google Drive with PyDrive2
How is the progress? Let's get on with the boom ?? in Python
Implement the Singleton pattern in Python
Play with Turtle on Google Colab
Get the width of the div on the server side with Selenium + PhantomJS + Python
Play the comment of Nico Nico Douga on the terminal in conjunction with the video
Create and edit spreadsheets in any folder on Google Drive with python
[Android] Display images on the web in the info Window of Google Map
Use The Metabolic Disassembler on Google Colaboratory
Display Python 3 in the browser with MAMP
Download files on the web with Python
Looking back on 2016 in the Crystal language
Machine learning with Pytorch on Google Colab
Get holidays with the Google Calendar API
Verify coordinates on Google Map with Geocoder
With Django + Google Cloud Strage, ImageField images are displayed normally on the server
I installed Pygame with Python 3.5.1 in the environment of pyenv on OS X
Get information on the 100 most influential tech Twitter users in the world with python.
Save Twitter's tweets with Geo in CSV and plot them on Google Map.
Log in to the remote server with SSH
[Python] Get the files in a folder with Python
Play with Google Spread Sheets in python (OAuth)
Determine the numbers in the image taken with the webcam
Detect folders with the same image in ImageHash
Try working with Mongo in Python on Mac
Load the network modeled with Rhinoceros in Python ②
Introduction to Python with Atom (on the way)
Implement the autocomplete feature on Django's admin screen
I tried playing with the calculator on tkinter
Load the network modeled with Rhinoceros in Python ①
Embed other images on the raster with ArcPy
The story that fits in with pip installation
How to set a shared folder with the host OS in CentOS7 on VirtualBOX
Put Scipy + Matplotlib in Ubuntu on Vagrant and display the graph with X11 Forwarding