Skip to content

Commit e15d9e8

Browse files
committed
Merge pull request #1 from kentaro/authenticator
Add support for GitHub
2 parents 3b01529 + 2b25299 commit e15d9e8

File tree

6 files changed

+268
-111
lines changed

6 files changed

+268
-111
lines changed

README.md

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# "gate" for your private resources
22

3-
gate is a static file server and reverse proxy integrated with google account authentications.
3+
gate is a static file server and reverse proxy integrated with OAuth2 account authentication.
44

5-
With gate, you can safely serve your private resources with your company google apps authenticaiton.
5+
With gate, you can safely serve your private resources based on whether or not request user is a member of your company's Google Apps or GitHub organizations.
66

77
## Usage
88

@@ -26,15 +26,20 @@ auth:
2626
# authentication key for cookie store
2727
key: secret123
2828

29-
google:
30-
# your google app keys
29+
info:
30+
# oauth2 provider name (`google` or `github`)
31+
service: google
32+
# your app keys for the service
3133
client_id: your client id
3234
client_secret: your client secret
33-
# your google app redirect_url: path is always "/oauth2callback"
35+
# your app redirect_url for the service: if the service is Google, path is always "/oauth2callback"
3436
redirect_url: https://yourapp.example.com/oauth2callback
3537

36-
# # restrict domain. (optional)
37-
# domain: yourdomain.com
38+
# # restrict user request. (optional)
39+
# restrictions:
40+
# - yourdomain.com # domain of your Google App (Google)
41+
# - [email protected] # specific email address (same as above)
42+
# - your_company_org # organization name (GitHub)
3843

3944
# document root for static files
4045
htdocs: ./
@@ -48,7 +53,44 @@ proxy:
4853
- path: /influxdb
4954
dest: http://127.0.0.1:8086
5055
strip_path: yes
56+
```
57+
58+
## Authentication Strategy
59+
60+
gate now supports Google Apps and GitHub to authenticate users.
61+
62+
### Example config for Google
63+
64+
```yaml
65+
auth:
66+
info:
67+
service: google
68+
client_id: your client id
69+
client_secret: your client secret
70+
redirect_url: https://yourapp.example.com/oauth2callback
71+
72+
# restrict user request. (optional)
73+
restrictions:
74+
- yourdomain.com # domain of your Google App
75+
- [email protected] # specific email address
76+
```
77+
78+
### Example config for GitHub
79+
80+
Unlike the example of Google Apps above, if the `service` is GitHub, gate uses whether request user is a member of organization designated like below:
81+
82+
```yaml
83+
auth:
84+
info:
85+
service: github
86+
client_id: your client id
87+
client_secret: your client secret
88+
redirect_url: https://yourapp.example.com/oauth2callback
5189
90+
# restrict user request. (optional)
91+
restrictions:
92+
- foo_organization
93+
- bar_organization
5294
```
5395

5496
## License

authenticator.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"github.com/go-martini/martini"
6+
gooauth2 "github.com/golang/oauth2"
7+
"github.com/martini-contrib/oauth2"
8+
"io/ioutil"
9+
"log"
10+
"net/http"
11+
"strings"
12+
)
13+
14+
type Authenticator interface {
15+
Authenticate([]string, martini.Context, oauth2.Tokens, http.ResponseWriter, *http.Request)
16+
Handler() martini.Handler
17+
}
18+
19+
func NewAuthenticator(conf *Conf) Authenticator {
20+
var authenticator Authenticator
21+
22+
if conf.Auth.Info.Service == "google" {
23+
handler := oauth2.Google(&gooauth2.Options{
24+
ClientID: conf.Auth.Info.ClientId,
25+
ClientSecret: conf.Auth.Info.ClientSecret,
26+
RedirectURL: conf.Auth.Info.RedirectURL,
27+
Scopes: []string{"email"},
28+
})
29+
authenticator = &GoogleAuth{&BaseAuth{handler}}
30+
} else if conf.Auth.Info.Service == "github" {
31+
handler := oauth2.Github(&gooauth2.Options{
32+
ClientID: conf.Auth.Info.ClientId,
33+
ClientSecret: conf.Auth.Info.ClientSecret,
34+
RedirectURL: conf.Auth.Info.RedirectURL,
35+
Scopes: []string{"read:org"},
36+
})
37+
authenticator = &GitHubAuth{&BaseAuth{handler}}
38+
} else {
39+
panic("unsupported authentication method")
40+
}
41+
42+
return authenticator
43+
}
44+
45+
type BaseAuth struct {
46+
handler martini.Handler
47+
}
48+
49+
func (b *BaseAuth) Handler() martini.Handler {
50+
return b.handler
51+
}
52+
53+
type GoogleAuth struct {
54+
*BaseAuth
55+
}
56+
57+
func (a *GoogleAuth) Authenticate(domain []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) {
58+
extra := tokens.ExtraData()
59+
if _, ok := extra["id_token"]; ok == false {
60+
log.Printf("id_token not found")
61+
forbidden(w)
62+
return
63+
}
64+
65+
keys := strings.Split(extra["id_token"], ".")
66+
if len(keys) < 2 {
67+
log.Printf("invalid id_token")
68+
forbidden(w)
69+
return
70+
}
71+
72+
data, err := base64Decode(keys[1])
73+
if err != nil {
74+
log.Printf("failed to decode base64: %s", err.Error())
75+
forbidden(w)
76+
return
77+
}
78+
79+
var info map[string]interface{}
80+
if err := json.Unmarshal(data, &info); err != nil {
81+
log.Printf("failed to decode json: %s", err.Error())
82+
forbidden(w)
83+
return
84+
}
85+
86+
if email, ok := info["email"].(string); ok {
87+
var user *User
88+
if len(domain) > 0 {
89+
for _, d := range domain {
90+
if strings.Contains(d, "@") {
91+
if d == email {
92+
user = &User{email}
93+
}
94+
} else {
95+
if strings.HasSuffix(email, "@"+d) {
96+
user = &User{email}
97+
break
98+
}
99+
}
100+
}
101+
} else {
102+
user = &User{email}
103+
}
104+
105+
if user != nil {
106+
log.Printf("user %s logged in", email)
107+
c.Map(user)
108+
} else {
109+
log.Printf("email doesn't allow: %s", email)
110+
forbidden(w)
111+
return
112+
}
113+
} else {
114+
log.Printf("email not found")
115+
forbidden(w)
116+
return
117+
}
118+
}
119+
120+
type GitHubAuth struct {
121+
*BaseAuth
122+
}
123+
124+
func (a *GitHubAuth) Authenticate(organizations []string, c martini.Context, tokens oauth2.Tokens, w http.ResponseWriter, r *http.Request) {
125+
if len(organizations) > 0 {
126+
req, err := http.NewRequest("GET", "https://api.github.com/user/orgs", nil)
127+
if err != nil {
128+
log.Printf("failed to create a request to retrieve organizations: %s", err)
129+
forbidden(w)
130+
return
131+
}
132+
133+
req.SetBasicAuth(tokens.Access(), "x-oauth-basic")
134+
135+
client := http.Client{}
136+
res, err := client.Do(req)
137+
if err != nil {
138+
log.Printf("failed to retrieve organizations: %s", err)
139+
forbidden(w)
140+
return
141+
}
142+
143+
data, err := ioutil.ReadAll(res.Body)
144+
res.Body.Close()
145+
146+
if err != nil {
147+
log.Printf("failed to read body of GitHub response: %s", err)
148+
forbidden(w)
149+
return
150+
}
151+
152+
var info []map[string]interface{}
153+
if err := json.Unmarshal(data, &info); err != nil {
154+
log.Printf("failed to decode json: %s", err.Error())
155+
forbidden(w)
156+
return
157+
}
158+
159+
for _, userOrg := range info {
160+
for _, org := range organizations {
161+
if userOrg["login"] == org {
162+
return
163+
}
164+
}
165+
}
166+
167+
log.Print("not a member of designated organizations")
168+
forbidden(w)
169+
return
170+
}
171+
}
172+
173+
func forbidden(w http.ResponseWriter) {
174+
w.WriteHeader(403)
175+
w.Write([]byte("Access denied"))
176+
}

conf.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import (
77
)
88

99
type Conf struct {
10-
Addr string `yaml:"address"`
11-
SSL SSLConf `yaml:"ssl"`
12-
Auth AuthConf `yaml:"auth"`
13-
Domain []string `yaml:"domain"`
14-
Proxies []ProxyConf `yaml:"proxy"`
15-
Htdocs string `yaml:"htdocs"`
10+
Addr string `yaml:"address"`
11+
SSL SSLConf `yaml:"ssl"`
12+
Auth AuthConf `yaml:"auth"`
13+
Restrictions []string `yaml:"restrictions"`
14+
Proxies []ProxyConf `yaml:"proxy"`
15+
Htdocs string `yaml:"htdocs"`
1616
}
1717

1818
type SSLConf struct {
@@ -22,14 +22,15 @@ type SSLConf struct {
2222

2323
type AuthConf struct {
2424
Session AuthSessionConf `yaml:"session"`
25-
Google AuthGoogleConf `yaml:"google"`
25+
Info AuthInfoConf `yaml:"info"`
2626
}
2727

2828
type AuthSessionConf struct {
2929
Key string `yaml:"key"`
3030
}
3131

32-
type AuthGoogleConf struct {
32+
type AuthInfoConf struct {
33+
Service string `yaml:"service"`
3334
ClientId string `yaml:"client_id"`
3435
ClientSecret string `yaml:"client_secret"`
3536
RedirectURL string `yaml:"redirect_url"`
@@ -59,14 +60,17 @@ func ParseConf(path string) (*Conf, error) {
5960
if c.Auth.Session.Key == "" {
6061
return nil, errors.New("auth.session.key config is required")
6162
}
62-
if c.Auth.Google.ClientId == "" {
63-
return nil, errors.New("auth.google.client_id config is required")
63+
if c.Auth.Info.Service == "" {
64+
return nil, errors.New("auth.info.service config is required")
6465
}
65-
if c.Auth.Google.ClientSecret == "" {
66-
return nil, errors.New("auth.google.client_secret config is required")
66+
if c.Auth.Info.ClientId == "" {
67+
return nil, errors.New("auth.info.client_id config is required")
6768
}
68-
if c.Auth.Google.RedirectURL == "" {
69-
return nil, errors.New("auth.google.redirect_url config is required")
69+
if c.Auth.Info.ClientSecret == "" {
70+
return nil, errors.New("auth.info.client_secret config is required")
71+
}
72+
if c.Auth.Info.RedirectURL == "" {
73+
return nil, errors.New("auth.info.redirect_url config is required")
7074
}
7175

7276
if c.Htdocs == "" {

config_sample.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ auth:
1111
# authentication key for cookie store
1212
key: secret123
1313

14-
google:
15-
# your google app keys
14+
info:
15+
# oauth2 provider name (`google` or `github`)
16+
service: google
17+
# your app keys for the service
1618
client_id: your client id
1719
client_secret: your client secret
18-
# your google app redirect_url: path is always "/oauth2callback"
20+
# your app redirect_url for the service: if the service is Google, path is always "/oauth2callback"
1921
redirect_url: https://yourapp.example.com/oauth2callback
2022

21-
# # restrict domain. (optional)
22-
# domain:
23-
# - yourdomain.com # restrict by domain
24-
# - [email protected] # or specific address
23+
# # restrict user request. (optional)
24+
# restrictions:
25+
# - yourdomain.com # domain of your Google App (Google)
26+
# - [email protected] # specific email address (same as above)
27+
# - your_company_org # organization name (GitHub)
2528

2629
# document root for static files
2730
htdocs: ./

0 commit comments

Comments
 (0)