Skip to content
This repository was archived by the owner on Mar 16, 2021. It is now read-only.

Commit f8086c7

Browse files
memorymxschmitt
authored andcommitted
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/
1 parent 912c53c commit f8086c7

File tree

7 files changed

+163
-35
lines changed

7 files changed

+163
-35
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
- Visitor Counting
1515
- Expirable Links
1616
- URL deletion
17-
- Authorization System via OAuth 2.0 (Google, GitHub and Microsoft)
17+
- Multiple authorization strategies:
18+
- Local authorization via OAuth 2.0 (Google, GitHub and Microsoft)
19+
- Proxy authorization for running behind e.g. [Google IAP](https://cloud.google.com/iap/)
1820
- Easy [ShareX](https://github.com/ShareX/ShareX) integration
1921
- Dockerizable
2022
- Multiple supported storage backends

build/config.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ RedisPassword: replace me # if using the redis backend, a conneciton password.
66
DataDir: ./data # Contains: the database and the private key
77
EnableDebugMode: true # Activates more detailed logging
88
ShortedIDLength: 10 # Length of the random generated ID which is used for new shortened URLs
9-
Google:
9+
AuthBackend: oauth # Can be 'oauth' or 'proxy'
10+
Google: # only relevant when using the oauth authbackend
1011
ClientID: replace me
1112
ClientSecret: replace me
12-
GitHub:
13+
GitHub: # only relevant when using the oauth authbackend
1314
ClientID: replace me
1415
ClientSecret: replace me
15-
Microsoft:
16+
Microsoft: # only relevant when using the oauth authbackend
1617
ClientID: replace me
1718
ClientSecret: 'replace me'
19+
Proxy: # only relevant when using the proxy authbackend
20+
RequireUserHeader: false # If true, will reject connections that do not have the UserHeader set
21+
UserHeader: "X-Goog-Authenticated-User-ID" # pull the unique user ID from this header
22+
DisplayNameHeader: "X-Goog-Authenticated-User-Email" # pull the display naem from this header

handlers/auth.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"fmt"
45
"net/http"
56

67
"github.com/mxschmitt/golang-url-shortener/handlers/auth"
@@ -36,6 +37,14 @@ func (h *Handler) initOAuth() {
3637
h.engine.POST("/api/v1/auth/check", h.handleAuthCheck)
3738
}
3839

40+
// initProxyAuth intializes data structures for proxy authentication mode
41+
func (h *Handler) initProxyAuth() {
42+
h.engine.Use(sessions.Sessions("backend", sessions.NewCookieStore(util.GetPrivateKey())))
43+
h.providers = []string{}
44+
h.providers = append(h.providers, "proxy")
45+
h.engine.POST("/api/v1/auth/check", h.handleAuthCheck)
46+
}
47+
3948
func (h *Handler) parseJWT(wt string) (*auth.JWTClaims, error) {
4049
token, err := jwt.ParseWithClaims(wt, &auth.JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
4150
return util.GetPrivateKey(), nil
@@ -49,7 +58,8 @@ func (h *Handler) parseJWT(wt string) (*auth.JWTClaims, error) {
4958
return token.Claims.(*auth.JWTClaims), nil
5059
}
5160

52-
func (h *Handler) authMiddleware(c *gin.Context) {
61+
// oAuthMiddleware implements an auth layer that validates a JWT token
62+
func (h *Handler) oAuthMiddleware(c *gin.Context) {
5363
authError := func() error {
5464
wt := c.GetHeader("Authorization")
5565
if wt == "" {
@@ -72,25 +82,94 @@ func (h *Handler) authMiddleware(c *gin.Context) {
7282
c.Next()
7383
}
7484

85+
// proxyAuthMiddleware implements an auth layer that trusts (and
86+
// optionally requires) header data from an identity-aware proxy
87+
func (h *Handler) proxyAuthMiddleware(c *gin.Context) {
88+
authError := func() error {
89+
claims, err := h.fakeClaimsForProxy(c)
90+
if err != nil {
91+
return err
92+
}
93+
c.Set("user", claims)
94+
return nil
95+
}()
96+
if authError != nil {
97+
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
98+
"error": "authentication failed",
99+
})
100+
logrus.Errorf("Authentication middleware check failed: %v\n", authError)
101+
return
102+
}
103+
c.Next()
104+
}
105+
106+
// fakeClaimsForProxy returns a pointer to a auth.JWTClaims struct containing
107+
// data pulled from headers inserted by an identity-aware proxy.
108+
func (h *Handler) fakeClaimsForProxy(c *gin.Context) (*auth.JWTClaims, error) {
109+
uid := c.GetHeader(util.GetConfig().Proxy.UserHeader)
110+
logrus.Debugf("Got proxy uid '%s' from header '%s'", uid, util.GetConfig().Proxy.UserHeader)
111+
if uid == "" {
112+
logrus.Debugf("No proxy uid found!")
113+
if util.GetConfig().Proxy.RequireUserHeader {
114+
msg := fmt.Sprintf("Required authorization header not set: %s", util.GetConfig().Proxy.UserHeader)
115+
logrus.Error(msg)
116+
return nil, errors.New(msg)
117+
}
118+
logrus.Debugf("Setting uid to 'anonymous'")
119+
uid = "anonymous"
120+
}
121+
// optionally pick a display name out of the headers as well; if we
122+
// can't find it, just use the uid.
123+
displayName := c.GetHeader(util.GetConfig().Proxy.DisplayNameHeader)
124+
logrus.Debugf("Got proxy display name '%s' from header '%s'", displayName, util.GetConfig().Proxy.DisplayNameHeader)
125+
if displayName == "" {
126+
logrus.Debugf("Setting displayname to '%s'", uid)
127+
displayName = uid
128+
}
129+
// it's not actually oauth but the naming convention is too
130+
// deeply embedded in the code for it to be worth changing.
131+
claims := &auth.JWTClaims{
132+
OAuthID: uid,
133+
OAuthName: displayName,
134+
OAuthPicture: "/images/proxy_user.png",
135+
OAuthProvider: "proxy",
136+
}
137+
return claims, nil
138+
}
139+
75140
func (h *Handler) handleAuthCheck(c *gin.Context) {
76141
var data struct {
77142
Token string `binding:"required"`
78143
}
79-
if err := c.ShouldBind(&data); err != nil {
144+
var claims *auth.JWTClaims
145+
var err error
146+
147+
if err = c.ShouldBind(&data); err != nil {
148+
logrus.Errorf("Did not bind correctly: %v", err)
80149
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
81150
return
82151
}
83-
claims, err := h.parseJWT(data.Token)
152+
if util.GetConfig().AuthBackend == "proxy" {
153+
// for proxy auth, we trust that the proxy has taken care of things
154+
// for us and we are only testing that the middleware successfully
155+
// pulled the necessary headers from the request.
156+
claims, err = h.fakeClaimsForProxy(c)
157+
} else {
158+
claims, err = h.parseJWT(data.Token)
159+
}
84160
if err != nil {
161+
logrus.Errorf("Could not parse auth data: %v", err)
85162
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
86163
return
87164
}
88-
c.JSON(http.StatusOK, gin.H{
165+
sessionData := gin.H{
89166
"ID": claims.OAuthID,
90167
"Name": claims.OAuthName,
91168
"Picture": claims.OAuthPicture,
92169
"Provider": claims.OAuthProvider,
93-
})
170+
}
171+
logrus.Debugf("Found session data: %v", sessionData)
172+
c.JSON(http.StatusOK, sessionData)
94173
}
95174

96175
func (h *Handler) oAuthPropertiesEquals(c *gin.Context, oauthID, oauthProvider string) bool {

handlers/handlers.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,16 @@ func New(store stores.Store) (*Handler, error) {
3939
if err := h.setHandlers(); err != nil {
4040
return nil, errors.Wrap(err, "could not set handlers")
4141
}
42-
if !DoNotPrivateKeyChecking {
43-
if err := util.CheckForPrivateKey(); err != nil {
44-
return nil, errors.Wrap(err, "could not check for private key")
42+
if util.GetConfig().AuthBackend == "oauth" {
43+
if !DoNotPrivateKeyChecking {
44+
if err := util.CheckForPrivateKey(); err != nil {
45+
return nil, errors.Wrap(err, "could not check for private key")
46+
}
4547
}
48+
h.initOAuth()
49+
} else if util.GetConfig().AuthBackend == "proxy" {
50+
h.initProxyAuth()
4651
}
47-
h.initOAuth()
4852
return h, nil
4953
}
5054

@@ -76,7 +80,15 @@ func (h *Handler) setHandlers() error {
7680
}
7781
h.engine.Use(ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, false))
7882
protected := h.engine.Group("/api/v1/protected")
79-
protected.Use(h.authMiddleware)
83+
if util.GetConfig().AuthBackend == "oauth" {
84+
logrus.Info("Using OAuth auth backend")
85+
protected.Use(h.oAuthMiddleware)
86+
} else if util.GetConfig().AuthBackend == "proxy" {
87+
logrus.Info("Using proxy auth backend")
88+
protected.Use(h.proxyAuthMiddleware)
89+
} else {
90+
logrus.Fatalf("Auth backend method '%s' is not recognized", util.GetConfig().AuthBackend)
91+
}
8092
protected.POST("/create", h.handleCreate)
8193
protected.POST("/lookup", h.handleLookup)
8294
protected.GET("/recent", h.handleRecent)

static/public/images/proxy_user.png

2.79 KB
Loading

static/src/index.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ import Visitors from './Visitors/Visitors'
1616
import util from './util/util'
1717
export default class BaseComponent extends Component {
1818
state = {
19-
oAuthPopupOpened: true,
19+
authPopupOpened: true,
2020
userData: {},
2121
authorized: false,
2222
activeItem: "",
23-
info: null
23+
info: {}
2424
}
2525

2626
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
2727

2828
onOAuthClose = () => {
29-
this.setState({ oAuthPopupOpened: true })
29+
this.setState({ authPopupOpened: true })
3030
}
3131

32-
componentWillMount() {
32+
componentDidMount() {
3333
fetch('/api/v1/info')
3434
.then(d => d.json())
3535
.then(info => this.setState({ info }))
@@ -85,16 +85,34 @@ export default class BaseComponent extends Component {
8585
}
8686
}
8787

88+
onProxyAuthOpen = () => {
89+
// the token contents don't matter for proxy auth, but
90+
// checkAuth() needs it to be set to something
91+
window.localStorage.setItem('token', {"lorem": "ipsum"});
92+
this.checkAuth();
93+
this.setState({ authPopupOpened: false })
94+
}
95+
8896
handleLogout = () => {
8997
window.localStorage.removeItem("token")
9098
this.setState({ authorized: false })
9199
}
92100

93101
render() {
94-
const { oAuthPopupOpened, authorized, activeItem, userData, info } = this.state
102+
const { authPopupOpened, authorized, activeItem, userData, info } = this.state
95103
if (!authorized) {
104+
if (Array.isArray(info.providers) && info.providers.includes("proxy")) {
105+
// window.localStorage.setItem('token', {"lorem": "ipsum"});
106+
// this.checkAuth();
107+
return (
108+
<Modal size='tiny' open={authPopupOpened} onMount={this.onProxyAuthOpen}>
109+
<Modal.Header>Authentication</Modal.Header>
110+
<Modal.Content><p>If you are seeing this, you have not successfully authenticated to the proxy.</p></Modal.Content>
111+
</Modal>
112+
)
113+
} else if (Array.isArray(info.providers)) {
96114
return (
97-
<Modal size='tiny' open={oAuthPopupOpened} onClose={this.onOAuthClose}>
115+
<Modal size='tiny' open={authPopupOpened} onClose={this.onOAuthClose}>
98116
<Modal.Header>
99117
Authentication
100118
</Modal.Header>
@@ -123,6 +141,7 @@ export default class BaseComponent extends Component {
123141
</Modal.Content>
124142
</Modal >
125143
)
144+
}
126145
}
127146
return (
128147
<HashRouter>
@@ -149,9 +168,11 @@ export default class BaseComponent extends Component {
149168
}}>
150169
About
151170
</Menu.Item>
152-
<Menu.Menu position='right'>
153-
<Menu.Item onClick={this.handleLogout}>Logout</Menu.Item>
154-
</Menu.Menu>
171+
<Menu.Menu position='right'>
172+
{userData.Name && <Menu.Item>{userData.Name}</Menu.Item>}
173+
{Array.isArray(info.providers) && !info.providers.includes("proxy") &&
174+
<Menu.Item onClick={this.handleLogout}>Logout</Menu.Item>}
175+
</Menu.Menu>
155176
</Menu>
156177
<Route exact path="/" component={Home} />
157178
<Route path="/about" render={() => <About info={info} />} />

util/config.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,33 @@ import (
1414

1515
// Configuration are the available config values
1616
type Configuration struct {
17-
ListenAddr string `yaml:"ListenAddr" env:"LISTEN_ADDR"`
18-
BaseURL string `yaml:"BaseURL" env:"BASE_URL"`
19-
DataDir string `yaml:"DataDir" env:"DATA_DIR"`
20-
Backend string `yaml:"Backend" env:"BACKEND"`
21-
RedisHost string `yaml:"RedisHost" env:"REDIS_HOST"`
22-
RedisPassword string `yaml:"RedisPassword" env:"REDIS_PASSWORD"`
23-
UseSSL bool `yaml:"EnableSSL" env:"USE_SSL"`
24-
EnableDebugMode bool `yaml:"EnableDebugMode" env:"ENABLE_DEBUG_MODE"`
25-
ShortedIDLength int `yaml:"ShortedIDLength" env:"SHORTED_ID_LENGTH"`
26-
Google oAuthConf `yaml:"Google" env:"GOOGLE"`
27-
GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"`
28-
Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"`
17+
ListenAddr string `yaml:"ListenAddr" env:"LISTEN_ADDR"`
18+
BaseURL string `yaml:"BaseURL" env:"BASE_URL"`
19+
DataDir string `yaml:"DataDir" env:"DATA_DIR"`
20+
Backend string `yaml:"Backend" env:"BACKEND"`
21+
RedisHost string `yaml:"RedisHost" env:"REDIS_HOST"`
22+
RedisPassword string `yaml:"RedisPassword" env:"REDIS_PASSWORD"`
23+
AuthBackend string `yaml:"AuthBackend" env:"AUTH_BACKEND"`
24+
UseSSL bool `yaml:"EnableSSL" env:"USE_SSL"`
25+
EnableDebugMode bool `yaml:"EnableDebugMode" env:"ENABLE_DEBUG_MODE"`
26+
ShortedIDLength int `yaml:"ShortedIDLength" env:"SHORTED_ID_LENGTH"`
27+
Google oAuthConf `yaml:"Google" env:"GOOGLE"`
28+
GitHub oAuthConf `yaml:"GitHub" env:"GITHUB"`
29+
Microsoft oAuthConf `yaml:"Microsoft" env:"MICROSOFT"`
30+
Proxy proxyAuthConf `yaml:"Proxy" env:"PROXY"`
2931
}
3032

3133
type oAuthConf struct {
3234
ClientID string `yaml:"ClientID" env:"CLIENT_ID"`
3335
ClientSecret string `yaml:"ClientSecret" env:"CLIENT_SECRET"`
3436
}
3537

38+
type proxyAuthConf struct {
39+
RequireUserHeader bool `yaml:"RequireUserHeader" env:"REQUIRE_USER_HEADER"`
40+
UserHeader string `yaml:"UserHeader" env:"USER_HEADER"`
41+
DisplayNameHeader string `yaml:"DisplayNameHeader" env:"DISPLAY_NAME_HEADER"`
42+
}
43+
3644
// config contains the default values
3745
var config = Configuration{
3846
ListenAddr: ":8080",
@@ -42,6 +50,7 @@ var config = Configuration{
4250
EnableDebugMode: false,
4351
UseSSL: false,
4452
ShortedIDLength: 4,
53+
AuthBackend: "oauth",
4554
}
4655

4756
// ReadInConfig loads the Configuration and other needed folders for further usage

0 commit comments

Comments
 (0)