Today I learned how to implement SSO using nginx auth_request module.

The basic idea is to autheticate a user based on the result of a suberequest to authentication server. If the subrequest returns 401, redirect the user to login authetication server. After the user login successfully, user info is stored in cookies to allow subsequent access to the web application.

Refer to “SSO with Nginx auth_request module” for details.

To autheticate a user, I implemented a simple Go program based on new web project generated by go-bootstrap.io. Then I added a handler function for /auth/ endpoint:

router.HandleFunc("/auth/{zone:.+}", handlers.GetAuth).Methods("GET")
func GetAuth(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")
  cookieStore := context.Get(r, "cookieStore").(*sessions.CookieStore)
  session, _ := cookieStore.Get(r, "sso-session")
  userI := session.Values["user"]
  if userI == nil {
    http.Error(w, fmt.Sprintf("unrecognized user"), http.StatusUnauthorized)
    return
  }
 
  currentUser := userI.(*models.UserRow)
  zone := mux.Vars(r)["zone"]
  for _, email := range accessByZone[zone] {
    if currentUser.Email == email {
      return // access granted
    }
  }
 
  logrus.Info("failed to autheticate:", zone, currentUser.Email)
  http.Error(w, fmt.Sprintf("%s is forbidden to access %s", currentUser.Email, zone), http.StatusForbidden)
}

Here is the nginx configuration. As you can see authencation (/sso) and application (/secret) are hosted under the same server and that's why cookie-based solution can work fine.

location /sso {
  rewrite /sso/(.*) /$1 break;

  proxy_pass http://localhost:8012;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header Host $http_host;
  proxy_set_header Client-IP $remote_addr;
  proxy_set_header X-Forwarded-For $remote_addr;
  proxy_set_header X-Upstream $remote_addr;
}

error_page 401 = @error401;
location @error401 {
   return 302 /sso/;
}

location /secret {
  auth_request /sso/auth/secret;
}