Previous Article: Pattern.

In this post, we’ll dive into back-end development by adding social logins to the app.

The way social logins work is simple: users click on a link and are redirected to the GitHub authorization page. When the user grants us access to their personal information, it is redirected back to the login page. The next time we try to log in, the system will not ask for authorization again, that is, our application has already remembered the user. This makes the login process look as fast as you can click with a mouse.

The process becomes more complicated when you consider the internal implementation. First, we need to register a new GitHub OAuth application.

The important thing in this step is to call back the URL. We set it to http://localhost:3000/api/oauth/github/callback. This is because, during development, we always work on localhost. Once you are ready to ship the application into production, register a new application with the correct callback URL.

Once registered, you will receive a “client ID” and a “security key”. To be safe, please do not share them with anyone 👀

Let’s start writing some code by the way. Now, create a main.go file:

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"

	"github.com/gorilla/securecookie"
	"github.com/joho/godotenv"
	"github.com/knq/jwt"
	_ "github.com/lib/pq"
	"github.com/matryer/way"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
)

var origin *url.URL
var db *sql.DB
var githubOAuthConfig *oauth2.Config
var cookieSigner *securecookie.SecureCookie
var jwtSigner jwt.Signer

func main() {
	godotenv.Load()

	port := intEnv("PORT", 3000)
	originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))
	databaseURL := env("DATABASE_URL"."Postgresql: / / [email protected]:26257 / messenger? sslmode=disable")
	githubClientID := os.Getenv("GITHUB_CLIENT_ID")
	githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
	hashKey := env("HASH_KEY"."secret")
	jwtKey := env("JWT_KEY"."secret")

	var err error
	iforigin, err = url.Parse(originString); err ! = nil || ! origin.IsAbs() {
		log.Fatal("invalid origin")
		return
	}

	if i, err := strconv.Atoi(origin.Port()); err == nil {
		port = i
	}

	if githubClientID == "" || githubClientSecret == "" {
		log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")
		return
	}

	if db, err = sql.Open("postgres", databaseURL); err ! = nil { log.Fatalf("could not open database connection: %v\n", err)
		return
	}
	defer db.Close()
	iferr = db.Ping(); err ! = nil { log.Fatalf("could not ping to db: %v\n", err)
		return
	}

	githubRedirectURL := *origin
	githubRedirectURL.Path = "/api/oauth/github/callback"
	githubOAuthConfig = &oauth2.Config{
		ClientID:     githubClientID,
		ClientSecret: githubClientSecret,
		Endpoint:     github.Endpoint,
		RedirectURL:  githubRedirectURL.String(),
		Scopes:       []string{"read:user"},
	}

	cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)

	jwtSigner, err = jwt.HS256.New([]byte(jwtKey))
	iferr ! = nil { log.Fatalf("could not create JWT signer: %v\n", err)
		return
	}

	router := way.NewRouter()
	router.HandleFunc("GET"."/api/oauth/github", githubOAuthStart)
	router.HandleFunc("GET"."/api/oauth/github/callback", githubOAuthCallback)
	router.HandleFunc("GET"."/api/auth_user", guard(getAuthUser))

	log.Printf("accepting connections on port %d\n", port)
	log.Printf("starting server at %s\n", origin.String())
	addr := fmt.Sprintf(":%d", port)
	iferr = http.ListenAndServe(addr, router); err ! = nil { log.Fatalf("could not start server: %v\n", err)
	}
}

func env(key, fallbackValue string) string {
	v, ok := os.LookupEnv(key)
	if! ok {return fallbackValue
	}
	return v
}

func intEnv(key string, fallbackValue int) int {
	v, ok := os.LookupEnv(key)
	if! ok {return fallbackValue
	}
	i, err := strconv.Atoi(v)
	iferr ! = nil {return fallbackValue
	}
	return i
}
Copy the code

Install dependencies:

go get -u github.com/gorilla/securecookie
go get -u github.com/joho/godotenv
go get -u github.com/knq/jwt
go get -u github.com/lib/pq
ge get -u github.com/matoous/go-nanoid
go get -u github.com/matryer/way
go get -u golang.org/x/oauth2
Copy the code

We’ll use the.env file to hold the key and other configuration. Please create this file and make sure it contains at least the following:

GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
Copy the code

Other environment variables we need to use are:

  • PORT: Port on which the server runs. The default value is3000.
  • ORIGIN: Your domain name, the default value ishttp://localhost:3000/. We can also specify ports here.
  • DATABASE_URL: The address of the Cockroach database. The default value isPostgresql: / / [email protected]:26257 / messenger? sslmode=disable.
  • HASH_KEY: Key used to sign cookies. Yes, we use a signed cookie to ensure security.
  • JWT_KEY: Used to sign JSONThe web tokenWeb TokenThe key.

You don’t need to write them to the.env file because the default values are already set in the code.

After reading the configuration and connecting to the database, we will create an OAuth configuration. We’ll use the ORIGIN information to build the callback URL (just like we registered on the GitHub page). Our data range is set to “read:user”. This will allow us to read public user information, in which case we only need their username and avatar. We then initialize cookies and JWT signers. Define some endpoints and start the server.

Before implementing the HTTP handler, let’s write some functions to send the HTTP response.

func respond(w http.ResponseWriter, v interface{}, statusCode int) {
	b, err := json.Marshal(v)
	iferr ! = nil { respondError(w, fmt.Errorf("could not marshal response: %v", err))
		return
	}
	w.Header().Set("Content-Type"."application/json; charset=utf-8")
	w.WriteHeader(statusCode)
	w.Write(b)
}

func respondError(w http.ResponseWriter, err error) {
	log.Println(err)
	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
Copy the code

The first function sends JSON, while the second logs the Error to the console and returns a 500 Internal Server Error message.

Request to start

So, users click on the link that says “Access with GitHub.” The link points to/API /oauth/ Github, which redirects the user to Github.

func githubOAuthStart(w http.ResponseWriter, r *http.Request) {
	state, err := gonanoid.Nanoid()
	iferr ! = nil { respondError(w, fmt.Errorf("could not generte state: %v", err))
		return
	}

	stateCookieValue, err := cookieSigner.Encode("state", state)
	iferr ! = nil { respondError(w, fmt.Errorf("could not encode state cookie: %v", err))
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     "state",
		Value:    stateCookieValue,
		Path:     "/api/oauth/github",
		HttpOnly: true,
	})
	http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
}
Copy the code

OAuth2 uses a mechanism to prevent CSRF attacks, so it requires a “state.” We use Nanoid() to create a random string and use this string as the state. We also save it as a cookie.

Request a callback

Once a user grants us access to his personal information, he will be redirected to this endpoint. The URL of the query string will contain state (state) and the authorization code (code) : / API/request/lot/callback? State = & code =.

const jwtLifetime = time.Hour * 24 * 14

type GithubUser struct {
	ID        int     `json:"id"`
	Login     string  `json:"login"`
	AvatarURL *string `json:"avatar_url,omitempty"`}type User struct {
	ID        string  `json:"id"`
	Username  string  `json:"username"`
	AvatarURL *string `json:"avatarUrl"`
}

func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
	stateCookie, err := r.Cookie("state")
	iferr ! = nil { http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     "state",
		Value:    "",
		MaxAge:   -1,
		HttpOnly: true,
	})

	var state string
	if err = cookieSigner.Decode("state", stateCookie.Value, &state); err ! = nil { http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)return
	}

	q := r.URL.Query()

	ifstate ! = q.Get("state") {
		http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
		return
	}

	ctx := r.Context()

	t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))
	iferr ! = nil { respondError(w, fmt.Errorf("could not fetch github token: %v", err))
		return
	}

	client := githubOAuthConfig.Client(ctx, t)
	resp, err := client.Get("https://api.github.com/user")
	iferr ! = nil { respondError(w, fmt.Errorf("could not fetch github user: %v", err))
		return
	}

	var githubUser GithubUser
	iferr = json.NewDecoder(resp.Body).Decode(&githubUser); err ! = nil { respondError(w, fmt.Errorf("could not decode github user: %v", err))
		return
	}
	defer resp.Body.Close()

	tx, err := db.BeginTx(ctx, nil)
	iferr ! = nil { respondError(w, fmt.Errorf("could not begin tx: %v", err))
		return
	}

	var user User
	if err = tx.QueryRowContext(ctx, `
		SELECT id, username, avatar_url FROM users WHERE github_id = The $1
	`, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {
		if err = tx.QueryRowContext(ctx, `
			INSERT INTO users (username, avatar_url, github_id) VALUES (The $1.$2.$3) RETURNING id `, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err ! = nil { respondError(w, fmt.Errorf("could not insert user: %v", err))
			return
		}
		user.Username = githubUser.Login
		user.AvatarURL = githubUser.AvatarURL
	} else iferr ! = nil { respondError(w, fmt.Errorf("could not query user by github ID: %v", err))
		return
	}

	iferr = tx.Commit(); err ! = nil { respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))
		return
	}

	exp := time.Now().Add(jwtLifetime)
	token, err := jwtSigner.Encode(jwt.Claims{
		Subject:    user.ID,
		Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),
	})
	iferr ! = nil { respondError(w, fmt.Errorf("could not create token: %v", err))
		return
	}

	expiresAt, _ := exp.MarshalText()

	data := make(url.Values)
	data.Set("token", string(token))
	data.Set("expires_at", string(expiresAt))

	http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)
}
Copy the code

First, we try to decode the cookie using the previously saved state. And compares it to the state in the query string. If they do not match, we return a 418 I’m teapot (unknown source) error.

Next, we use the authorization code to generate a token. This token is used to create HTTP clients that make requests to the GitHub API. So in the end we will send a GET request to https://api.github.com/user. This endpoint will provide us with the currently authenticated user information in JSON format. We will decode this and retrieve the user’s ID, login name (username), and avatar URL.

We will then try to find the user with that GitHub ID in the database. If none is found, a new one is created using the data.

Then, for the newly created user, we issue a JSON network token with the user ID as the Subject and use this token to redirect to the front end, including the Expiration date of the token in the query string.

The Web application will also be used in other posts, but the redirected link will be /callback? Token = & expires_at =. From there, we will use JavaScript to retrieve the token and expiration date from the URL and make GET requests to/API /auth_user in terms of Bearer token_here tokens in the Authorization header, To get the authenticated user and save it to localStorage.

Guard the middleware

To capture currently authenticated users, we designed Guard middleware. This is because in the following articles, we will have many endpoints that require authentication, and the middleware will allow us to share this functionality.

type ContextKey struct {
	Name string
}

var keyAuthUserID = ContextKey{"auth_user_id"}

func guard(handler http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var token string
		if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {
			token = a[7:]
		} else if t := r.URL.Query().Get("token"); t ! ="" {
			token = t
		} else {
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
			return
		}

		var claims jwt.Claims
		iferr := jwtSigner.Decode([]byte(token), &claims); err ! = nil { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)return
		}

		ctx := r.Context()
		ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)

		handler(w, r.WithContext(ctx))
	}
}
Copy the code

First, we try to read the token from either the Authorization header or the token field in the URL query string. If not, we need to return the 401 Unauthorized error. We will then decode the claim in the token and use the subject as the currently authenticated user ID.

Now, we can use this middleware to encapsulate any HTTP.handlerFunc that needs authorization and keep the authenticated user ID in the context of the handler function.

var guarded = guard(func(w http.ResponseWriter, r *http.Request) {
    authUserID := r.Context().Value(keyAuthUserID).(string)
})
Copy the code

Obtaining an Authentication User

func getAuthUser(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	authUserID := ctx.Value(keyAuthUserID).(string)

	var user User
	if err := db.QueryRowContext(ctx, `
		SELECT username, avatar_url FROM users WHERE id = The $1
	`, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {
		http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
		return
	} else iferr ! = nil { respondError(w, fmt.Errorf("could not query auth user: %v", err))
		return
	}

	user.ID = authUserID

	respond(w, user, http.StatusOK)
}
Copy the code

We used Guard middleware to retrieve the currently authenticated user ID and query the database.

This section covers the back-end OAuth process. In the next post, we’ll see how to start conversations with other users.

  • The source code

Via: nicolasparada.netlify.com/posts/go-me…

By Nicolas Parada, Lujun9972

This article is originally compiled by LCTT and released in Linux China