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.
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.
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.
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.
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.
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