In this post we will review using how to secure access to multiple web application using the Google OAuth 2.0. The post was created based information from several sites, each providing a part of the solution:
See also my previous posts about related issues:
Why do we need to use Google OAuth 2.0 ?
Once a project goes online, its services are accessible to all users. In case it is in the cloud as public service, its services are available to the entire world. While some of the services, such as the public faced web server, allow access to everyone, other services, such as the management services, should be secured, and allow access only to your organization admins.
The following diagram demonstrates an example of a project which includes several public facing services, and several management restricted services.
To secure the management restricted services, we want to use authentication. This means that anyone accessing these services should authenticate.
One way to achieve this is to implement authentication in each service. However this approach has several problems. First, we need to invest time in out proprietary services, to implement authentication. Second, for the non-proprietary 3rd party services, we can only use the authentication methods that were provided by these 3rd party tools. Third, we would probably want to create a central authentication service, instead of configuring users and passwords on each service.
A better alternative is to integrate with an OAuth service, such as Google OAuth 2.0 service. As demonstrated in the diagram below, access to the management restricted services is verified by an NGINX reverse proxy that enables access only to a specific list of authenticated users. This means that Google OAuth 2.0 service handles the authentication of the users, using a user/password and any other authentication method, such as MFA.
What is the authentication flow?
The following diagram displays the authentication flow.
The steps in the user authentication are as follows:
- The user accesses a management site, e.g. prometheus.mydomain.com
- The ingress routes the request to the NGINX reverse proxy
- The NGINX reverse proxy sends an auth_request to the authentication service
- The authentication service finds a first time incoming request without any authentication headers, and returns HTTP status 401 - unauthorized
- The NGINX reverse proxy redirects the user to Google OAuth 2.0 service
- The user logins to its account in the Google OAuth 2.0 site
- The Google OAuth 2.0 site redirects the user back to the management site, and adds an access code header in the query string
- The user accesses a management site, e.g. prometheus.mydomain.com
- The ingress routes the request to the NGINX reverse proxy
- The NGINX reverse proxy sends an auth_request to the authentication service
- The authentication service finds the access code header, send verification request to the Google OAuth 2.0 service
- The Google OAuth 2.0 service returns that the access code is valid
- The authentication service sends a user info request to Google service
- The Google service returns the user information, including the user email
- The authentication service finds the user email is allowed access, creates and store locally a access token to enable fast access for future requests in the same session, and returns the access token as well as HTTP status code 200
- The NGINX permits access to the management site
- The management site returns its response
- The NGINX reverse proxy adds the access token as a session cookie
Create a Google OAuth 2.0 Client ID
To create a client ID, login to
Google Developers Console, and create a new project in case you do not already have one. Then click on the menu hamburger on the top left, and select API & Services, and Credentials.
Click on Create Credentials, and then select OAuth 2.0 Client ID.
Select Web Application as the application type, and add the URIs that will be used to access the site.
In case using multiple backend services, each with a different domain name, you will need to to add each of the domain names. You would probably want to add another URI, such as localhost:8080 for debug purposes.
Once created, you will be notified with the client ID and client secret. Keep these values for use in the later steps.
The authentication service
I have implemented the authentication service in GO.
First we need to create an HTTP server.
package main
import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"io/ioutil"
"log"
"math/rand"
"net/http"
)
var tokens = make(map[string]bool)
type UserInfo struct {
Email string `json:"email"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/auth-callback", handler)
server := &http.Server{
Addr: fmt.Sprintf(":8000"),
Handler: mux,
}
log.Printf("Starting HTTP Server. Listening at %q", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("%v", err)
} else {
log.Println("Server closed!")
}
}
Next add a handler for the auth request. It receives the Google Auth code from the query string, for validation vs the Google Auth API. The auth_token is used to prevent multiple access to Google Auth on the same session. It will be set as a session cookie by the NGINX reverse proxy.
func handler(response http.ResponseWriter, request *http.Request) {
code := request.URL.Query().Get("code")
redirectUri := request.URL.Query().Get("redirect_uri")
token := request.Header["auth_token"][0]
token, err := authenticate(token, code, redirectUri)
if err != nil {
response.WriteHeader(http.StatusUnauthorized)
response.Write([]byte(err.Error()))
} else {
response.Header().Set("auth_token", token)
response.Write([]byte("access allowed"))
}
}
Implement the token validation code:
func authenticate(token string, code string, redirectUri string) (string, error) {
if token != "" && tokens[token] {
// user already has a session cookie - allow access
return token, nil
}
err := accessOAuth(code, redirectUri)
if err != nil {
return "", fmt.Errorf("oauth validation failed: %v", err)
}
randomToken := randSeq(128)
tokens[randomToken] = true
return randomToken, nil
}
func randSeq(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
And finally implement Google API usage.
I am just validating a hard coded email, but you should add your own application code. You can count on the fact the the user email is indeed as specified, but you still want to allow access only to specified list of your organization administrators.
func accessOAuth(code string, redirectUri string) error {
if code == "" {
return fmt.Errorf("empty oauth code provided, will not access oauth")
}
googleOauthConfig := oauth2.Config{
RedirectURL: redirectUri,
ClientID: "YOUR_CLIENT_ID_HERE e.g. 574647965688-634v8s2k6pnfhlto4245bna4ib5aj6o0.apps.googleusercontent.com",
ClientSecret: "YOUR_CLIENT_SECRET_HERE e.g. Wc3WH4RHlPK-j32fu1w8JciD",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: google.Endpoint,
}
token, err := googleOauthConfig.Exchange(context.Background(), code)
if err != nil {
return fmt.Errorf("exchange failed: %v", err)
}
url := "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken
response, err := http.Get(url)
if err != nil {
return fmt.Errorf("get user info failed: %v", err)
}
defer response.Body.Close()
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("read get user info response failed: %v", err)
}
info := UserInfo{}
err = json.Unmarshal(bytes, &info)
if err != nil {
return fmt.Errorf("unmarshal failed: %v", err)
}
// replace with your own email validation method
if info.Email == "my_allowed_email@google.com" {
return fmt.Errorf("email %v is not permitted", info.Email)
}
return nil
}
The NGINX reverse proxy
We should configure NGINX to orchestrate the authentication flow. In this example, I've only added the prometheus.my-domain.com as a backend service, but you should duplicate this for each restricted backend service that you have.
user nginx;
worker_processes 10;
error_log /dev/stdout debug;
pid /var/run/nginx.pid;
load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;
events {
worker_connections 10240;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
resolver 8.8.8.8;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log off;
sendfile on;
port_in_redirect off;
upstream prometheus {
server prometheus-service;
keepalive 1000;
keepalive_requests 1000000;
keepalive_timeout 300s;
}
upstream auth {
server auth-service;
keepalive 1000;
keepalive_requests 1000000;
keepalive_timeout 300s;
}
server {
listen 8080;
server_name localhost;
location /health {
return 200 'NGINX is alive';
}
}
server {
listen 8080;
server_name prometheus.my-domain.com;
location = /auth {
internal;
proxy_method GET;
set $query '';
if ($request_uri ~* "[^\?]+\?(.*)$") {
set $query $1;
}
proxy_pass http://auth/auth-callback?redirect_uri=http%3A//prometheus.my-domain.com:30026&$query;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header auth_token $cookie_auth_token;
}
error_page 401 = @error401;
location @error401 {
return 302 https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A//www.googleapis.com/auth/userinfo.email&response_type=code&redirect_uri=http%3A//prometheus.my-domain.com:30026&client_id=574647965688-7l5v8s2k7pnfhlto42ilbna4ib5kk6o0.apps.googleusercontent.com;
}
location / {
auth_request /auth;
auth_request_set $auth_token $upstream_http_auth_token;
add_header Set-Cookie "auth_token=$auth_token;Path=/";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://prometheus;
proxy_set_header Connection "";
proxy_ignore_headers "Cache-Control" "Expires";
proxy_buffers 32 4m;
proxy_busy_buffers_size 25m;
proxy_buffer_size 512k;
client_max_body_size 10m;
client_body_buffer_size 4m;
proxy_connect_timeout 300;
proxy_read_timeout 300;
proxy_send_timeout 300;
proxy_intercept_errors off;
proxy_http_version 1.1;
}
}
}
We include two upstreams in the configuration: the backend service (prometheus), and the auth service.
Access the the backend service is protected using the auth_request directive, which on its turns, send a request the our authentication service. If the authentication service returns HTTP status 401, we redirect the user to Google's login page. Otherwise, we set a session cookie with the authentication service auth_token response.
Final Notes
In this post we have reviewed a complete solution for securing multiple applications using NGINX as a reverse proxy, and Google OAuth 2.0 as the user authentication authority.
In case using Ingress, you will need to configure the restricted management applications domains to be routed to the NGINX reverse proxy, instead of directly to management applications services.