initial commit
This commit is contained in:
283
vendor/github.com/ericchiang/oidc/oidcproxy/main.go
generated
vendored
Normal file
283
vendor/github.com/ericchiang/oidc/oidcproxy/main.go
generated
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ericchiang/oidc"
|
||||
"github.com/gorilla/securecookie"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieName = "oidc-proxy"
|
||||
// This header will be set by oidcproxy during authentication and
|
||||
// passed to the backend.
|
||||
emailHeaderName = "X-User-Email"
|
||||
)
|
||||
|
||||
// Session represents a logged in user's active session.
|
||||
type Session struct {
|
||||
Email string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(&Session{})
|
||||
}
|
||||
|
||||
var (
|
||||
// Flags.
|
||||
issuer string
|
||||
backend string
|
||||
scopes string
|
||||
allow string
|
||||
httpAddr string
|
||||
httpsAddr string
|
||||
cookieExp time.Duration
|
||||
|
||||
// Set up during initial configuration.
|
||||
oauth2Config = new(oauth2.Config)
|
||||
oidcProvider *oidc.Provider
|
||||
backendHandler *httputil.ReverseProxy
|
||||
verifier *oidc.IDTokenVerifier
|
||||
|
||||
// Regexps of emails to allow.
|
||||
allowEmail []*regexp.Regexp
|
||||
|
||||
nonceSource *memNonceSource
|
||||
|
||||
cookieEncrypter *securecookie.SecureCookie
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&issuer, "issuer", "https://accounts.google.com", "The issuer URL of the OpenID Connect provider.")
|
||||
flag.StringVar(&backend, "backend", "", "The URL of the backened to proxy to.")
|
||||
flag.StringVar(&oauth2Config.RedirectURL, "redirect-url", "", "A full OAuth2 redirect URL.")
|
||||
flag.StringVar(&oauth2Config.ClientID, "client-id", "", "The client ID of the OAuth2 client.")
|
||||
flag.StringVar(&oauth2Config.ClientSecret, "client-secret", "", "The client secret of the OAuth2 client.")
|
||||
flag.StringVar(&scopes, "scopes", "openid,email,profile", `A comma seprated list of OAuth2 scopes to request ("openid" required).`)
|
||||
flag.StringVar(&allow, "allow-email", ".*", "Comma seperated list of email regexp's to match for access to the backend.")
|
||||
flag.StringVar(&httpAddr, "http", "127.0.0.1:5556", "Default address to listen on.")
|
||||
flag.DurationVar(&cookieExp, "cookie-exp", time.Hour*24, "Duration for which a login cookie is valid for.")
|
||||
flag.Parse()
|
||||
|
||||
// Set flags from environment variables.
|
||||
flag.VisitAll(func(f *flag.Flag) {
|
||||
if f.Value.String() != f.DefValue {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert flag name, e.g. "redirect-url" becomes "OIDC_PROXY_REDIRECT_URL"
|
||||
envVar := "OIDC_PROXY_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))
|
||||
|
||||
if envVal := os.Getenv(envVar); envVal != "" {
|
||||
if err := flag.Set(f.Name, envVal); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
// All flags are manditory.
|
||||
if f.Value.String() == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
})
|
||||
|
||||
// compile email regexps
|
||||
for _, expr := range strings.Split(allow, ",") {
|
||||
allowEmailRegexp, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid regexp: %q %v", expr, err)
|
||||
}
|
||||
allowEmail = append(allowEmail, allowEmailRegexp)
|
||||
}
|
||||
|
||||
// configure reverse proxy
|
||||
backendURL, err := url.Parse(backend)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse backend: %v", err)
|
||||
}
|
||||
backendHandler = httputil.NewSingleHostReverseProxy(backendURL)
|
||||
|
||||
redirectURL, err := url.Parse(oauth2Config.RedirectURL)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse redirect URL: %v", err)
|
||||
}
|
||||
|
||||
// Query for the provider.
|
||||
oidcProvider, err = oidc.NewProvider(context.TODO(), issuer)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get provider: %v", err)
|
||||
}
|
||||
|
||||
nonceSource = newNonceSource(context.TODO())
|
||||
verifier = oidcProvider.NewVerifier(context.TODO(), oidc.VerifyNonce(nonceSource))
|
||||
|
||||
oauth2Config.Endpoint = oidcProvider.Endpoint()
|
||||
oauth2Config.Scopes = strings.Split(scopes, ",")
|
||||
|
||||
// Initialize secure cookies.
|
||||
// TODO(ericchiang): make these configurable
|
||||
hashKey := make([]byte, 64)
|
||||
blockKey := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, hashKey); err != nil {
|
||||
log.Fatalf("failed to initialize hash key: %v", err)
|
||||
}
|
||||
if _, err := io.ReadFull(rand.Reader, blockKey); err != nil {
|
||||
log.Fatalf("failed to initialize block key: %v", err)
|
||||
}
|
||||
cookieEncrypter = securecookie.New(hashKey, blockKey)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", handleProxy)
|
||||
mux.HandleFunc("/login", handleRedirect)
|
||||
mux.HandleFunc("/logout", handleLogout)
|
||||
mux.HandleFunc(redirectURL.Path, handleCallback)
|
||||
|
||||
log.Printf("Listening on: %s", httpAddr)
|
||||
http.ListenAndServe(httpAddr, mux)
|
||||
}
|
||||
|
||||
// httpRedirect returns a handler which redirects to the provided path.
|
||||
func httpRedirect(path string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, path, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
// httpError returns a handler which presents an error to the end user.
|
||||
func httpError(status int, format string, a ...interface{}) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf(format, a...), http.StatusInternalServerError)
|
||||
})
|
||||
}
|
||||
|
||||
func handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
func() http.Handler {
|
||||
state := r.URL.Query().Get("state")
|
||||
if state == "" {
|
||||
log.Printf("State not set")
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
if err := nonceSource.ClaimNonce(state); err != nil {
|
||||
log.Printf("Failed to claim nonce: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
|
||||
oauth2Token, err := oauth2Config.Exchange(context.TODO(), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
log.Printf("Failed to exchange token: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
|
||||
// Extract the ID Token from oauth2 token.
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
log.Println("No ID Token found")
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
|
||||
payload, err := verifier.Verify(rawIDToken)
|
||||
if err != nil {
|
||||
log.Printf("Failed to verify token: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
log.Printf("Failed to decode claims: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
if !claims.EmailVerified || claims.Email == "" {
|
||||
log.Println("Failed to verify email")
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
|
||||
s := Session{Email: claims.Email, Expires: time.Now().Add(cookieExp)}
|
||||
encoded, err := cookieEncrypter.Encode(cookieName, s)
|
||||
if err != nil {
|
||||
log.Printf("Failed to encrypt session: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Authentication failed")
|
||||
}
|
||||
|
||||
// Set the encoded cookie
|
||||
cookie := &http.Cookie{Name: cookieName, Value: encoded, HttpOnly: true, Path: "/"}
|
||||
http.SetCookie(w, cookie)
|
||||
return httpRedirect("/")
|
||||
|
||||
}().ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func handleRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(ericchiang): since arbitrary requests can create nonces, rate limit this endpoint.
|
||||
func() http.Handler {
|
||||
nonce, err := nonceSource.Nonce()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create nonce: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Failed to generate redirect")
|
||||
}
|
||||
state, err := nonceSource.Nonce()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create state: %v", err)
|
||||
return httpError(http.StatusInternalServerError, "Failed to generate redirect")
|
||||
}
|
||||
return httpRedirect(oauth2Config.AuthCodeURL(state, oauth2.ApprovalForce, oidc.Nonce(nonce)))
|
||||
}().ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
cookie := &http.Cookie{Name: cookieName, Value: "", HttpOnly: true, Path: "/"}
|
||||
http.SetCookie(w, cookie)
|
||||
httpRedirect("/login").ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func handleProxy(w http.ResponseWriter, r *http.Request) {
|
||||
func() http.Handler {
|
||||
cookie, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
// Only error can be ErrNoCookie https://goo.gl/o5fZ49
|
||||
return httpRedirect("/login")
|
||||
}
|
||||
var s Session
|
||||
if err := cookieEncrypter.Decode(cookieName, cookie.Value, &s); err != nil {
|
||||
log.Printf("Failed to decode cookie: %v", err)
|
||||
return http.HandlerFunc(handleLogout) // clear the cookie
|
||||
}
|
||||
if time.Now().After(s.Expires) {
|
||||
log.Printf("Cookie for %q expired", s.Email)
|
||||
return http.HandlerFunc(handleLogout) // clear the cookie
|
||||
}
|
||||
|
||||
for _, allow := range allowEmail {
|
||||
if allow.MatchString(s.Email) {
|
||||
r.Header.Set(emailHeaderName, s.Email)
|
||||
return backendHandler
|
||||
}
|
||||
}
|
||||
log.Printf("Denying %q", s.Email)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := []byte(`<html><head></head><body>Provided email does not have permission to login. <a href="/logout">Try a different account.</a></body></html>`)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(resp)))
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write(resp)
|
||||
})
|
||||
}().ServeHTTP(w, r)
|
||||
}
|
72
vendor/github.com/ericchiang/oidc/oidcproxy/nonce.go
generated
vendored
Normal file
72
vendor/github.com/ericchiang/oidc/oidcproxy/nonce.go
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
gcInterval = time.Minute
|
||||
expiresIn = time.Minute * 10
|
||||
)
|
||||
|
||||
type memNonceSource struct {
|
||||
mu sync.Mutex
|
||||
nonces map[string]time.Time
|
||||
}
|
||||
|
||||
func newNonceSource(ctx context.Context) *memNonceSource {
|
||||
s := &memNonceSource{nonces: make(map[string]time.Time)}
|
||||
go s.garbageCollect(ctx)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *memNonceSource) Nonce() (string, error) {
|
||||
buff := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, buff); err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := base64.RawURLEncoding.EncodeToString(buff)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nonces[nonce] = time.Now().Add(expiresIn)
|
||||
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
func (s *memNonceSource) ClaimNonce(nonce string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.nonces[nonce]; ok {
|
||||
delete(s.nonces, nonce)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid nonce")
|
||||
}
|
||||
|
||||
func (s *memNonceSource) garbageCollect(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(gcInterval):
|
||||
s.mu.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for nonce, exp := range s.nonces {
|
||||
if now.After(exp) {
|
||||
delete(s.nonces, nonce)
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user