Instantly share code, notes, and snippets.

@bati11 /README.md
Last active Jun 24, 2018

Embed
What would you like to do?
Google SignIn + Server App (Go) https://blog.bati11.info/entry/2018/06/25/001711

サーバーサイドアプリケーションを起動する。

$ go run server.go

CLIENT_ID をGoogleで取得したclient_idに置き換える。その後クライアントサイドのdevサーバーを起動する。

$ npm start

http://localhost:3000 にアクセス。

{
"name": "google-sign-sample",
"version": "0.1.0",
"dependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.4",
"react-google-login": "^3.2.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
}
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)
// LoginParams は `POST /login` のリクエストJSONです
type LoginParams struct {
IDToken string
}
// LoginAccount は `POST /login` のレスポンスJSONです。
type LoginAccount struct {
UserID string `json:"userId"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
// IDTokenPayload はGoogleのトークンエンドポイントのレスポンスJSONです
type IDTokenPayload struct {
Iss string `json:"iss"`
Sub string `json:"sub"`
Azp string `json:"azp"`
Aud string `json:"aud"`
Iat string `json:"iat"`
Exp string `json:"exp"`
AtHash string `json:"at_hash"`
Jti string `json:"jti"`
Alg string `json:"alg"`
Kid string `json:"kid"`
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
Name string `json:"name"`
Picture string `json:"picture"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Locale string `json:"locale"`
}
// User ユーザー情報
type User struct {
ID string
Name string
CreatedAt time.Time
}
var sessionDB = make(map[string]string)
var userDB = make(map[string]User)
func findUser(userID string) *User {
user, ok := userDB[userID]
if !ok {
return nil
}
return &user
}
func findOrCreateUser(userID string, name string) User {
user := findUser(userID)
if user == nil {
user = &User{
ID: userID,
Name: name,
CreatedAt: time.Now(),
}
userDB[userID] = *user
}
return *user
}
func createSessionKey() (string, error) {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// GetMyProfile `/my` エンドポイントの処理です
func GetMyProfile(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("SessionKey")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
session, ok := sessionDB[cookie.Value]
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
user := findUser(session)
if user == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(user); err != nil {
panic(err)
}
}
// Login `/login` エンドポイントの処理です
func Login(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "OPTIONS":
w.Header().Add("Access-Control-Allow-Methods", "POST")
case "POST":
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
panic(err)
}
defer r.Body.Close()
var loginParams LoginParams
if err := json.Unmarshal(body, &loginParams); err != nil {
log.Println(body)
log.Println(err)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusInternalServerError)
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
return
}
// ID Tokenの検証を実施する
values := url.Values{
"id_token": {loginParams.IDToken},
}
resp, err := http.Get("https://www.googleapis.com/oauth2/v3/tokeninfo" + "?" + values.Encode())
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var idTokenPayload IDTokenPayload
if err := json.Unmarshal(body, &idTokenPayload); err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusInternalServerError)
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
return
}
// TODO idTokenPayload.Audが正しいかどうかチェックする
log.Println(idTokenPayload)
user := findOrCreateUser(idTokenPayload.Sub, idTokenPayload.Name)
sessionKey, err := createSessionKey()
if err != nil {
panic(err)
}
sessionDB[sessionKey] = user.ID
cookie := &http.Cookie{
Name: "SessionKey",
Value: sessionKey,
Path: "/",
Secure: false, // 実際はhttpsにしてtrueする
HttpOnly: true,
}
http.SetCookie(w, cookie)
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusNotFound)
}
}
func withCORS(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") // クライアントサイドのdevサーバーはlocalhost:3000で動いてる想定
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "true")
fn(w, r)
}
}
type withCORSHandler struct {
handler http.Handler
}
func main() {
http.HandleFunc("/my", withCORS(GetMyProfile))
http.HandleFunc("/login", withCORS(Login))
log.Fatal(http.ListenAndServe(":5555", nil))
}
// src/App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
import { GoogleLogin } from 'react-google-login';
const PrivateRoute = ({authenticated, render, ...rest}) => (
authenticated ? (
<Route {...rest} render={render} />
) : (
<Route
{...rest}
render={props =>
<Redirect
to={{
pathname: "/login",
state: {from: props.location}
}}
/>
}
/>
)
);
class App extends Component {
state = {
authenticated: false,
initialized: null
}
componentDidMount() {
fetch("http://localhost:5555/my", {
mode: 'cors',
credentials: 'include',
})
.then(response => {
if (response.status === 200) {
response.json().then(data => console.log(data))
this.setState({ authenticated: true, initialized: true })
} else {
this.setState({ authenticated: false, initialized: true })
}
})
}
onSignIn = (googleUser) => {
console.log('ID: ' + googleUser.getBasicProfile().getId());
fetch("http://localhost:5555/login", {
method: "POST",
mode: 'cors',
credentials: 'include',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ "IDToken": googleUser.getAuthResponse().id_token }),
})
.then(response => {
return fetch("http://localhost:5555/my", {
mode: 'cors',
credentials: 'include',
})
})
.then(response => {
response.json().then(data => console.log(data))
this.setState({authenticated: true})
})
}
onSignOut = () => {
fetch("http://localhost:5555/logout", {
method: "POST",
mode: 'cors',
credentials: 'include',
headers: new Headers({ 'Content-Type': 'application/json' }),
})
.finally(response => {
console.log(response)
this.setState({authenticated: false})
})
}
render() {
if (!this.state.initialized) {
return ('Loading...')
}
return (
<Router>
<Switch>
<Route exact path="/" render={props =>
<Top {...props} authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
}/>
<Route path="/login" render={props =>
<Login {...props} onSignIn={this.onSignIn} authenticated={this.state.authenticated} />
}/>
<PrivateRoute path="/private" authenticated={this.state.authenticated} render={props =>
<div>プライベートなページ</div>
}/>
</Switch>
</Router>
)
}
}
export default App;
class Login extends Component {
render() {
if (this.props.authenticated) {
const { from } = this.props.location.state || { from: { pathname: "/" } };
return (<Redirect to={from} />)
}
return (
<div>
<h1>ようこそ!</h1>
<div>
<GoogleLogin
clientId="<CLIENT_ID>"
buttonText="Login"
onSuccess={this.props.onSignIn}
onFailure={(err) => console.error(err)}
/>
</div>
<Link to='/'>Topへ</Link>
</div>
)
}
}
class Top extends Component {
render() {
if (this.props.authenticated) {
return (
<div>
<h1>ログイン済み:Topページ</h1>
<button onClick={this.props.onSignOut}>ログアウト</button>
</div>
)
} else {
return (
<div>
<h1>ログイン前:Topページ</h1>
<Link to='/login'>サインイン</Link>
</div>
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment