Browse Source

support using an identity-aware proxy for auth (#101)

Rather than directly fetching and verifying OAuth assertions, assume
that the app is running behind an authenticating proxy, and trust
headers that are set by the proxy.

- add config support for an "authbackend" directive, supporting either
  "oauth" or "proxy" as values; the "proxy" setting selects our new codepath
- add initProxyAuth and proxyAuthMiddleware methods to the Handler struct
- rename authMiddleWare to oAuthMiddleware in the Handler struct
- construct a faked auth.JWTClaims object when in proxy mode
- update Handler.handleAuthCheck() to return useful info in proxy mode
- add a fallback user icon for proxy mode
- implement check for proxy mode in index.js

See for example and reference:

https://cloud.google.com/iap/docs/identity-howto
https://cloud.google.com/beyondcorp/
dependabot/npm_and_yarn/web/prismjs-1.21.0
memory 8 years ago
committed by Max Schmitt
parent
commit
f8086c7492
  1. 4
      README.md
  2. 11
      build/config.yaml
  3. 89
      handlers/auth.go
  4. 22
      handlers/handlers.go
  5. BIN
      static/public/images/proxy_user.png
  6. 39
      static/src/index.js
  7. 33
      util/config.go

4
README.md

@ -14,7 +14,9 @@
- Visitor Counting
- Expirable Links
- URL deletion
- Authorization System via OAuth 2.0 (Google, GitHub and Microsoft)
- Multiple authorization strategies:
- Local authorization via OAuth 2.0 (Google, GitHub and Microsoft)
- Proxy authorization for running behind e.g. [Google IAP](https://cloud.google.com/iap/)
- Easy [ShareX](https://github.com/ShareX/ShareX) integration
- Dockerizable
- Multiple supported storage backends

11
build/config.yaml

@ -6,12 +6,17 @@ RedisPassword: replace me # if using the redis backend, a conneciton password.
DataDir: ./data # Contains: the database and the private key
EnableDebugMode: true # Activates more detailed logging
ShortedIDLength: 10 # Length of the random generated ID which is used for new shortened URLs
Google:
AuthBackend: oauth # Can be 'oauth' or 'proxy'
Google: # only relevant when using the oauth authbackend
ClientID: replace me
ClientSecret: replace me
GitHub:
GitHub: # only relevant when using the oauth authbackend
ClientID: replace me
ClientSecret: replace me
Microsoft:
Microsoft: # only relevant when using the oauth authbackend
ClientID: replace me
ClientSecret: 'replace me'
Proxy: # only relevant when using the proxy authbackend
RequireUserHeader: false # If true, will reject connections that do not have the UserHeader set
UserHeader: "X-Goog-Authenticated-User-ID" # pull the unique user ID from this header
DisplayNameHeader: "X-Goog-Authenticated-User-Email" # pull the display naem from this header

89
handlers/auth.go

@ -1,6 +1,7 @@
package handlers
import (
"fmt"
"net/http"
"github.com/mxschmitt/golang-url-shortener/handlers/auth"
@ -36,6 +37,14 @@ func (h *Handler) initOAuth() {
h.engine.POST("/api/v1/auth/check", h.handleAuthCheck)
}
// initProxyAuth intializes data structures for proxy authentication mode
func (h *Handler) initProxyAuth() {
h.engine.Use(sessions.Sessions("backend", sessions.NewCookieStore(util.GetPrivateKey())))
h.providers = []string{}
h.providers = append(h.providers, "proxy")
h.engine.POST("/api/v1/auth/check", h.handleAuthCheck)
}
func (h *Handler) parseJWT(wt string) (*auth.JWTClaims, error) {
token, err := jwt.ParseWithClaims(wt, &auth.JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return util.GetPrivateKey(), nil
@ -49,7 +58,8 @@ func (h *Handler) parseJWT(wt string) (*auth.JWTClaims, error) {
return token.Claims.(*auth.JWTClaims), nil
}
func (h *Handler) authMiddleware(c *gin.Context) {
// oAuthMiddleware implements an auth layer that validates a JWT token
func (h *Handler) oAuthMiddleware(c *gin.Context) {
authError := func() error {
wt := c.GetHeader("Authorization")
if wt == "" {
@ -72,25 +82,94 @@ func (h *Handler) authMiddleware(c *gin.Context) {
c.Next()
}
// proxyAuthMiddleware implements an auth layer that trusts (and
// optionally requires) header data from an identity-aware proxy
func (h *Handler) proxyAuthMiddleware(c *gin.Context) {
authError := func() error {
claims, err := h.fakeClaimsForProxy(c)
if err != nil {
return err
}
c.Set("user", claims)
return nil
}()
if authError != nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "authentication failed",
})
logrus.Errorf("Authentication middleware check failed: %v\n", authError)
return
}
c.Next()
}
// fakeClaimsForProxy returns a pointer to a auth.JWTClaims struct containing
// data pulled from headers inserted by an identity-aware proxy.
func (h *Handler) fakeClaimsForProxy(c *gin.Context) (*auth.JWTClaims, error) {
uid := c.GetHeader(util.GetConfig().Proxy.UserHeader)
logrus.Debugf("Got proxy uid '%s' from header '%s'", uid, util.GetConfig().Proxy.UserHeader)
if uid == "" {
logrus.Debugf("No proxy uid found!")
if util.GetConfig().Proxy.RequireUserHeader {
msg := fmt.Sprintf("Required authorization header not set: %s", util.GetConfig().Proxy.UserHeader)
logrus.Error(msg)
return nil, errors.New(msg)
}
logrus.Debugf("Setting uid to 'anonymous'")
uid = "anonymous"
}
// optionally pick a display name out of the headers as well; if we
// can't find it, just use the uid.
displayName := c.GetHeader(util.GetConfig().Proxy.DisplayNameHeader)
logrus.Debugf("Got proxy display name '%s' from header '%s'", displayName, util.GetConfig().Proxy.DisplayNameHeader)
if displayName == "" {
logrus.Debugf("Setting displayname to '%s'", uid)
displayName = uid
}
// it's not actually oauth but the naming convention is too
// deeply embedded in the code for it to be worth changing.
claims := &auth.JWTClaims{
OAuthID: uid,
OAuthName: displayName,
OAuthPicture: "/images/proxy_user.png",
OAuthProvider: "proxy",
}
return claims, nil
}
func (h *Handler) handleAuthCheck(c *gin.Context) {
var data struct {
Token string `binding:"required"`
}
if err := c.ShouldBind(&data); err != nil {
var claims *auth.JWTClaims
var err error
if err = c.ShouldBind(&data); err != nil {
logrus.Errorf("Did not bind correctly: %v", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
claims, err := h.parseJWT(data.Token)
if util.GetConfig().AuthBackend == "proxy" {
// for proxy auth, we trust that the proxy has taken care of things
// for us and we are only testing that the middleware successfully
// pulled the necessary headers from the request.
claims, err = h.fakeClaimsForProxy(c)
} else {
claims, err = h.parseJWT(data.Token)
}
if err != nil {
logrus.Errorf("Could not parse auth data: %v", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
sessionData := gin.H{
"ID": claims.OAuthID,
"Name": claims.OAuthName,
"Picture": claims.OAuthPicture,
"Provider": claims.OAuthProvider,
})
}
logrus.Debugf("Found session data: %v", sessionData)
c.JSON(http.StatusOK, sessionData)
}
func (h *Handler) oAuthPropertiesEquals(c *gin.Context, oauthID, oauthProvider string) bool {

22
handlers/handlers.go

@ -39,12 +39,16 @@ func New(store stores.Store) (*Handler, error) {
if err := h.setHandlers(); err != nil {
return nil, errors.Wrap(err, "could not set handlers")
}
if !DoNotPrivateKeyChecking {
if err := util.CheckForPrivateKey(); err != nil {
return nil, errors.Wrap(err, "could not check for private key")
if util.GetConfig().AuthBackend == "oauth" {
if !DoNotPrivateKeyChecking {
if err := util.CheckForPrivateKey(); err != nil {
return nil, errors.Wrap(err, "could not check for private key")
}
}
h.initOAuth()
} else if util.GetConfig().AuthBackend == "proxy" {
h.initProxyAuth()
}
h.initOAuth()
return h, nil
}
@ -76,7 +80,15 @@ func (h *Handler) setHandlers() error {
}
h.engine.Use(ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, false))
protected := h.engine.Group("/api/v1/protected")
protected.Use(h.authMiddleware)
if util.GetConfig().AuthBackend == "oauth" {
logrus.Info("Using OAuth auth backend")
protected.Use(h.oAuthMiddleware)
} else if util.GetConfig().AuthBackend == "proxy" {
logrus.Info("Using proxy auth backend")
protected.Use(h.proxyAuthMiddleware)
} else {
logrus.Fatalf("Auth backend method '%s' is not recognized", util.GetConfig().AuthBackend)
}
protected.POST("/create", h.handleCreate)
protected.POST("/lookup", h.handleLookup)
protected.GET("/recent", h.handleRecent)

BIN
static/public/images/proxy_user.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

39
static/src/index.js

@ -16,20 +16,20 @@ import Visitors from './Visitors/Visitors'
import util from './util/util'
export default class BaseComponent extends Component {
state = {
oAuthPopupOpened: true,
authPopupOpened: true,
userData: {},
authorized: false,
activeItem: "",
info: null
info: {}
}
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
onOAuthClose = () => {
this.setState({ oAuthPopupOpened: true })
this.setState({ authPopupOpened: true })
}
componentWillMount() {
componentDidMount() {
fetch('/api/v1/info')
.then(d => d.json())
.then(info => this.setState({ info }))
@ -85,16 +85,34 @@ export default class BaseComponent extends Component {
}
}
onProxyAuthOpen = () => {
// the token contents don't matter for proxy auth, but
// checkAuth() needs it to be set to something
window.localStorage.setItem('token', {"lorem": "ipsum"});
this.checkAuth();
this.setState({ authPopupOpened: false })
}
handleLogout = () => {
window.localStorage.removeItem("token")
this.setState({ authorized: false })
}
render() {
const { oAuthPopupOpened, authorized, activeItem, userData, info } = this.state
const { authPopupOpened, authorized, activeItem, userData, info } = this.state
if (!authorized) {
if (Array.isArray(info.providers) && info.providers.includes("proxy")) {
// window.localStorage.setItem('token', {"lorem": "ipsum"});
// this.checkAuth();
return (
<Modal size='tiny' open={authPopupOpened} onMount={this.onProxyAuthOpen}>
<Modal.Header>Authentication</Modal.Header>
<Modal.Content><p>If you are seeing this, you have not successfully authenticated to the proxy.</p></Modal.Content>
</Modal>
)
} else if (Array.isArray(info.providers)) {
return (
<Modal size='tiny' open={oAuthPopupOpened} onClose={this.onOAuthClose}>
<Modal size='tiny' open={authPopupOpened} onClose={this.onOAuthClose}>
<Modal.Header>
Authentication
</Modal.Header>
@ -123,6 +141,7 @@ export default class BaseComponent extends Component {
</Modal.Content>
</Modal >
)
}
}
return (
<HashRouter>
@ -149,9 +168,11 @@ export default class BaseComponent extends Component {
}}>
About
</Menu.Item>
<Menu.Menu position='right'>
<Menu.Item onClick={this.handleLogout}>Logout</Menu.Item>
</Menu.Menu>
<Menu.Menu position='right'>
{userData.Name && <Menu.Item>{userData.Name}</Menu.Item>}
{Array.isArray(info.providers) && !info.providers.includes("proxy") &&
<Menu.Item onClick={this.handleLogout}>Logout</Menu.Item>}
</Menu.Menu>
</Menu>
<Route exact path="/" component={Home} />
<Route path="/about" render={() => <About info={info} />} />

33
util/config.go

@ -14,18 +14,20 @@ import (
// Configuration are the available config values
type Configuration struct {
ListenAddr string `yaml:"ListenAddr" env:"LISTEN_ADDR"`
BaseURL string `yaml:"BaseURL" env:"BASE_URL"`
DataDir string `yaml:"DataDir" env:"DATA_DIR"`
Backend string `yaml:"Backend" env:"BACKEND"`
RedisHost string `yaml:"RedisHost" env:"REDIS_HOST"`
RedisPassword string `yaml:"RedisPassword" env:"REDIS_PASSWORD"`
UseSSL bool `yaml:"EnableSSL" env:"USE_SSL"`
EnableDebugMode bool `yaml:"EnableDebugMode" env:"ENABLE_DEBUG_MODE"`
ShortedIDLength int `yaml:"ShortedIDLength" env:"SHORTED_ID_LENGTH"`
Google oAuthConf `yaml:"Google" env:"GOOGLE"`
GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"`
Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"`
ListenAddr string `yaml:"ListenAddr" env:"LISTEN_ADDR"`
BaseURL string `yaml:"BaseURL" env:"BASE_URL"`
DataDir string `yaml:"DataDir" env:"DATA_DIR"`
Backend string `yaml:"Backend" env:"BACKEND"`
RedisHost string `yaml:"RedisHost" env:"REDIS_HOST"`
RedisPassword string `yaml:"RedisPassword" env:"REDIS_PASSWORD"`
AuthBackend string `yaml:"AuthBackend" env:"AUTH_BACKEND"`
UseSSL bool `yaml:"EnableSSL" env:"USE_SSL"`
EnableDebugMode bool `yaml:"EnableDebugMode" env:"ENABLE_DEBUG_MODE"`
ShortedIDLength int `yaml:"ShortedIDLength" env:"SHORTED_ID_LENGTH"`
Google oAuthConf `yaml:"Google" env:"GOOGLE"`
GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"`
Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"`
Proxy proxyAuthConf `yaml:"Proxy" env:"PROXY"`
}
type oAuthConf struct {
@ -33,6 +35,12 @@ type oAuthConf struct {
ClientSecret string `yaml:"ClientSecret" env:"CLIENT_SECRET"`
}
type proxyAuthConf struct {
RequireUserHeader bool `yaml:"RequireUserHeader" env:"REQUIRE_USER_HEADER"`
UserHeader string `yaml:"UserHeader" env:"USER_HEADER"`
DisplayNameHeader string `yaml:"DisplayNameHeader" env:"DISPLAY_NAME_HEADER"`
}
// config contains the default values
var config = Configuration{
ListenAddr: ":8080",
@ -42,6 +50,7 @@ var config = Configuration{
EnableDebugMode: false,
UseSSL: false,
ShortedIDLength: 4,
AuthBackend: "oauth",
}
// ReadInConfig loads the Configuration and other needed folders for further usage

Loading…
Cancel
Save