├── fixtures ├── github-user.json ├── testconfigwithnousers.json └── testconfig.json ├── public ├── denied.html ├── auth0.html.tpl └── github.html.tpl ├── array.go ├── .gitignore ├── cmd └── authproxy │ ├── config.json │ └── main.go ├── configuration_reader_test.go ├── configuration_reader.go ├── github_auth_context_test.go ├── templates_helper.go ├── authentication_context.go ├── LICENSE ├── configuration.go ├── listeners.go ├── configuration_test.go ├── auth0_auth_context.go ├── github_auth_context.go ├── README.md └── templates.go /fixtures/github-user.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "KensoDev", 3 | "id": 1, 4 | "created_at": "2009-04-30T09:29:26Z", 5 | "updated_at": "2017-07-11T17:31:17Z" 6 | } 7 | -------------------------------------------------------------------------------- /public/denied.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |6 | Your access has been denied, you are not an authorized user in this system 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /array.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | func inArray(val string, array []string) (exists bool) { 4 | exists = false 5 | 6 | for _, v := range array { 7 | if val == v { 8 | exists = true 9 | return 10 | } 11 | } 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/testconfigwithnousers.json: -------------------------------------------------------------------------------- 1 | { 2 | "upstreams": [ 3 | { 4 | "prefix": "/", 5 | "type": "static", 6 | "location": "static" 7 | }, 8 | { 9 | "type": "server", 10 | "prefix": "/api", 11 | "location": "http://localhost:4040" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /public/auth0.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |6 | Auth0 authentication proxy 7 |
8 |9 | Login to begin! 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /public/github.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |6 | Github Authentication Proxy 7 |
8 |9 | Login to begin! 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | cmd/authproxy/authproxy 17 | cmd/authproxy/.auth0 18 | cmd/authproxy/.github 19 | -------------------------------------------------------------------------------- /fixtures/testconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "authContext": "github", 3 | "users": [ 4 | { 5 | "username": "KensoDev" 6 | }, 7 | { 8 | "username": "KensoDev2", 9 | "restrict": "GET" 10 | } 11 | ], 12 | "upstreams": [ 13 | { 14 | "prefix": "/", 15 | "type": "static", 16 | "location": "static" 17 | }, 18 | { 19 | "type": "server", 20 | "prefix": "/api", 21 | "location": "http://localhost:4040" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /cmd/authproxy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authContext": "auth0", 3 | "users": [ 4 | { 5 | "username": "KensoDev" 6 | }, 7 | { 8 | "username": "KensoDev2", 9 | "restrict": "GET" 10 | } 11 | ], 12 | "upstreams": [ 13 | { 14 | "type": "http", 15 | "prefix": "/", 16 | "location": "https://bdf682c0.ngrok.io" 17 | }, 18 | { 19 | "type": "http", 20 | "prefix": "/api/", 21 | "location": "https://6cc1b022.ngrok.io" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /configuration_reader_test.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | "testing" 6 | ) 7 | 8 | func TestConfigurationReader(t *testing.T) { TestingT(t) } 9 | 10 | type ConfigurationReaderSuite struct{} 11 | 12 | var _ = Suite(&ConfigurationReaderSuite{}) 13 | 14 | func (s *ConfigurationReaderSuite) TestJsonReadFile(c *C) { 15 | configLocation := "fixtures/testconfig.json" 16 | reader := NewConfigurationReader(configLocation) 17 | data, err := reader.ReadConfigurationFile() 18 | c.Assert(err, IsNil) 19 | c.Assert(len(data), Not(Equals), 0) 20 | } 21 | -------------------------------------------------------------------------------- /configuration_reader.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | type ConfigurationReader struct { 9 | FileLocation string 10 | } 11 | 12 | func NewConfigurationReader(location string) *ConfigurationReader { 13 | return &ConfigurationReader{ 14 | FileLocation: location, 15 | } 16 | } 17 | 18 | func (r *ConfigurationReader) ReadConfigurationFile() ([]byte, error) { 19 | data, err := ioutil.ReadFile(r.FileLocation) 20 | 21 | if err != nil { 22 | return nil, fmt.Errorf("Error reading the file: %s", err.Error()) 23 | } 24 | 25 | return data, nil 26 | } 27 | -------------------------------------------------------------------------------- /github_auth_context_test.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | func TestGithubAuthContext(t *testing.T) { TestingT(t) } 11 | 12 | type GithubAuthContextSuite struct{} 13 | 14 | var _ = Suite(&GithubAuthContextSuite{}) 15 | 16 | func (s *GithubAuthContextSuite) TestGithubUserParsing(c *C) { 17 | data, err := ioutil.ReadFile("fixtures/github-user.json") 18 | configuration := &Configuration{} 19 | authContext := NewGithubAuthContext(configuration) 20 | user, err := authContext.ParseUserResponse(data) 21 | c.Assert(err, IsNil) 22 | c.Assert(user.UserName, Equals, "KensoDev") 23 | } 24 | -------------------------------------------------------------------------------- /templates_helper.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | ) 7 | 8 | // Don't edit the following line. This allows go generate ./... to work. 9 | //go:generate go-bindata -pkg authproxy -o templates.go public/ 10 | 11 | // RenderTemplate is a generic text/template render wrapper. 12 | func RenderTemplate(tpl string, data interface{}) (b []byte, err error) { 13 | // Templates don't actually need names. 14 | t, err := template.New("").Parse(tpl) 15 | if err != nil { 16 | return b, err 17 | } 18 | 19 | buf := &bytes.Buffer{} 20 | 21 | err = t.Execute(buf, data) 22 | if err != nil { 23 | return b, nil 24 | } 25 | 26 | return buf.Bytes(), nil 27 | } 28 | -------------------------------------------------------------------------------- /authentication_context.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | type AuthenticationContext interface { 10 | IsAccessTokenValidAndUserAuthorized(accessToken string) bool 11 | GetUserName(accessToken string) string 12 | GetHTTPEndpointPrefix() string 13 | GetCookieName() string 14 | GetLoginPage() ([]byte, error) 15 | ServeHTTP(w http.ResponseWriter, req *http.Request) 16 | RenderHTMLFile() error 17 | } 18 | 19 | // GetenvOrDie is a safety wrapper around os.Getenv. 20 | // This fatals if the key is unset or empty. 21 | func GetenvOrDie(k string) string { 22 | o := os.Getenv(k) 23 | 24 | if o == "" { 25 | log.Fatalf("%s environment variable missing and is required.", k) 26 | } 27 | 28 | return o 29 | } 30 | -------------------------------------------------------------------------------- /cmd/authproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/kensodev/micro-auth-proxy" 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | ) 11 | 12 | var ( 13 | listenPort = kingpin.Flag("listen", "Which port should the proxy listen on").Required().Int() 14 | configLocation = kingpin.Flag("config", "Proxy Config Location").Required().String() 15 | ) 16 | 17 | func main() { 18 | kingpin.Parse() 19 | 20 | configReader := authproxy.NewConfigurationReader(*configLocation) 21 | 22 | data, err := configReader.ReadConfigurationFile() 23 | handleError(err) 24 | 25 | config, err := authproxy.NewConfiguration(data) 26 | handleError(err) 27 | 28 | authproxy.NewHttpListeners(config) 29 | 30 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *listenPort), nil); err != nil { 31 | fmt.Errorf("Could not listen on port %s: %s", *listenPort, err.Error()) 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func handleError(err error) { 37 | if err != nil { 38 | fmt.Printf("Error reading your configuration file: %s", err.Error()) 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Avi Zurel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Configuration struct { 9 | AuthenticationContextName string `json:"authContext"` 10 | Upstreams []Upstream `json:"upstreams"` 11 | Users []User `json:"users"` 12 | } 13 | 14 | type User struct { 15 | Username string `json:"username"` 16 | Restrict string `json:"restrict"` 17 | } 18 | 19 | type Upstream struct { 20 | Prefix string `json:"prefix"` 21 | Location string `json:"location"` 22 | Type string `json:"type"` 23 | } 24 | 25 | func (c *Configuration) GetAuthenticationContext() (cx AuthenticationContext, err error) { 26 | if c.AuthenticationContextName == "github" { 27 | cx = NewGithubAuthContext(c) 28 | } 29 | 30 | if c.AuthenticationContextName == "auth0" { 31 | cx = NewAuth0AuthContext(c) 32 | } 33 | 34 | err = cx.RenderHTMLFile() 35 | return cx, err 36 | } 37 | 38 | func NewConfiguration(data []byte) (*Configuration, error) { 39 | config := &Configuration{} 40 | err := json.Unmarshal(data, config) 41 | 42 | if err != nil { 43 | return nil, fmt.Errorf("Problem with parsing the confi json file: %s", err.Error()) 44 | } 45 | 46 | if len(config.Users) == 0 { 47 | return nil, fmt.Errorf("You have no users configured") 48 | 49 | } 50 | 51 | return config, nil 52 | } 53 | 54 | func (c *Configuration) GetRestrictionsForUsername(username string) string { 55 | for _, user := range c.Users { 56 | if user.Username == username { 57 | return user.Restrict 58 | break 59 | } 60 | } 61 | 62 | return "NotAllowed" 63 | } 64 | 65 | func (c *Configuration) ShouldRestrictUser(username string, method string) bool { 66 | allowedMethod := c.GetRestrictionsForUsername(username) 67 | 68 | // Allowed all methods 69 | if allowedMethod == "" { 70 | return true 71 | } 72 | 73 | return allowedMethod == method 74 | } 75 | -------------------------------------------------------------------------------- /listeners.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | ) 9 | 10 | type Listener struct { 11 | Prefix string 12 | Location string 13 | Proxy *httputil.ReverseProxy 14 | Hostname string 15 | AuthContext AuthenticationContext 16 | Config *Configuration 17 | } 18 | 19 | func NewHttpListeners(config *Configuration) { 20 | authContext, err := config.GetAuthenticationContext() 21 | if err != nil { 22 | log.Fatalln("auth context creation failed:", err) 23 | } 24 | 25 | http.Handle(authContext.GetHTTPEndpointPrefix(), authContext) 26 | 27 | for _, upstream := range config.Upstreams { 28 | uri, _ := url.Parse(upstream.Location) 29 | 30 | proxy := httputil.NewSingleHostReverseProxy(uri) 31 | 32 | listener := &Listener{ 33 | AuthContext: authContext, 34 | Prefix: upstream.Prefix, 35 | Location: upstream.Location, 36 | Proxy: proxy, 37 | Hostname: uri.Hostname(), 38 | Config: config, 39 | } 40 | 41 | http.Handle(listener.Prefix, listener) 42 | } 43 | } 44 | 45 | func (l *Listener) ServeHTTP(w http.ResponseWriter, req *http.Request) { 46 | cookie, err := req.Cookie(l.AuthContext.GetCookieName()) 47 | 48 | if err != nil { 49 | auth, _ := l.AuthContext.GetLoginPage() 50 | w.Write(auth) 51 | return 52 | } 53 | 54 | token := cookie.Value 55 | 56 | if !l.AuthContext.IsAccessTokenValidAndUserAuthorized(token) { 57 | denied, _ := publicDeniedHtmlBytes() 58 | w.Write(denied) 59 | return 60 | } 61 | 62 | l.ServeAuthenticatedRequest(w, req, token) 63 | } 64 | 65 | func (l *Listener) ServeAuthenticatedRequest(w http.ResponseWriter, req *http.Request, accessToken string) { 66 | username := l.AuthContext.GetUserName(accessToken) 67 | allowed := l.Config.ShouldRestrictUser(username, req.Method) 68 | 69 | if !allowed { 70 | http.Error(w, "Your user is not allowed to perform this action", http.StatusUnauthorized) 71 | return 72 | } 73 | 74 | director := l.Proxy.Director 75 | l.Proxy.Director = func(req *http.Request) { 76 | director(req) 77 | req.Host = l.Hostname 78 | } 79 | 80 | l.Proxy.ServeHTTP(w, req) 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /configuration_test.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | "testing" 6 | ) 7 | 8 | func TestConfiguration(t *testing.T) { TestingT(t) } 9 | 10 | type ConfigurationSuite struct{} 11 | 12 | var _ = Suite(&ConfigurationSuite{}) 13 | 14 | func (s *ConfigurationSuite) TestJsonReadFile(c *C) { 15 | configLocation := "fixtures/testconfig.json" 16 | reader := NewConfigurationReader(configLocation) 17 | data, _ := reader.ReadConfigurationFile() 18 | 19 | config, _ := NewConfiguration(data) 20 | c.Assert(len(config.Upstreams), Equals, 2) 21 | } 22 | 23 | func (s *ConfigurationSuite) TestJsonReadFileForUsers(c *C) { 24 | configLocation := "fixtures/testconfig.json" 25 | reader := NewConfigurationReader(configLocation) 26 | data, _ := reader.ReadConfigurationFile() 27 | 28 | config, _ := NewConfiguration(data) 29 | c.Assert(len(config.Users), Equals, 2) 30 | } 31 | 32 | func (s *ConfigurationSuite) TestAuthenticationContext(c *C) { 33 | configLocation := "fixtures/testconfig.json" 34 | reader := NewConfigurationReader(configLocation) 35 | data, _ := reader.ReadConfigurationFile() 36 | 37 | config, _ := NewConfiguration(data) 38 | c.Assert(config.AuthenticationContextName, Equals, "github") 39 | } 40 | 41 | func (s *ConfigurationSuite) TestJsonReadFileForUsersAndValidateError(c *C) { 42 | configLocation := "fixtures/testconfigwithnousers.json" 43 | reader := NewConfigurationReader(configLocation) 44 | data, _ := reader.ReadConfigurationFile() 45 | 46 | _, err := NewConfiguration(data) 47 | c.Assert(err, Not(Equals), nil) 48 | } 49 | 50 | func (s *ConfigurationSuite) TestUserWithNoRestriction(c *C) { 51 | configLocation := "fixtures/testconfig.json" 52 | reader := NewConfigurationReader(configLocation) 53 | data, _ := reader.ReadConfigurationFile() 54 | 55 | config, _ := NewConfiguration(data) 56 | c.Assert(len(config.Users), Equals, 2) 57 | c.Assert(config.Users[0].Username, Equals, "KensoDev") 58 | c.Assert(config.Users[0].Restrict, Equals, "") 59 | c.Assert(config.Users[1].Restrict, Equals, "GET") 60 | } 61 | 62 | func (s *ConfigurationSuite) GetUserRestrictedMethod(c *C) { 63 | configLocation := "fixtures/testconfig.json" 64 | reader := NewConfigurationReader(configLocation) 65 | data, _ := reader.ReadConfigurationFile() 66 | 67 | config, _ := NewConfiguration(data) 68 | method := config.GetRestrictionsForUsername("KensoDev2") 69 | c.Assert(method, Equals, "Get") 70 | 71 | method = config.GetRestrictionsForUsername("KensoDev") 72 | c.Assert(method, Equals, "") 73 | } 74 | 75 | func (s *ConfigurationSuite) GetUserRestrictedMethodNotAllowed(c *C) { 76 | configLocation := "fixtures/testconfig.json" 77 | reader := NewConfigurationReader(configLocation) 78 | data, _ := reader.ReadConfigurationFile() 79 | 80 | config, _ := NewConfiguration(data) 81 | method := config.GetRestrictionsForUsername("KensoDev23234234") 82 | c.Assert(method, Equals, "NotAllowed") 83 | } 84 | 85 | func (s *ConfigurationSuite) TestShouldAllowedMethodForUsername(c *C) { 86 | configLocation := "fixtures/testconfig.json" 87 | reader := NewConfigurationReader(configLocation) 88 | data, _ := reader.ReadConfigurationFile() 89 | 90 | config, _ := NewConfiguration(data) 91 | should := config.ShouldRestrictUser("KensoDev", "POST") 92 | c.Assert(should, Equals, true) 93 | 94 | should = config.ShouldRestrictUser("KensoDev2", "POST") 95 | c.Assert(should, Equals, false) 96 | } 97 | -------------------------------------------------------------------------------- /auth0_auth_context.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | type Auth0AuthContext struct { 14 | Config *Configuration 15 | AuthDomain string 16 | ClientID string 17 | ClientSecret string 18 | CallbackURL string 19 | ValidAccessTokens map[string]string 20 | HTMLFile []byte 21 | } 22 | 23 | func NewAuth0AuthContext(config *Configuration) *Auth0AuthContext { 24 | return &Auth0AuthContext{ 25 | Config: config, 26 | ClientID: GetenvOrDie("AUTH0_CLIENT_ID"), 27 | ClientSecret: GetenvOrDie("AUTH0_CLIENT_SECRET"), 28 | AuthDomain: GetenvOrDie("AUTH0_DOMAIN"), 29 | CallbackURL: GetenvOrDie("AUTH0_CALLBACK_URL"), 30 | ValidAccessTokens: map[string]string{}, 31 | } 32 | } 33 | 34 | func (c *Auth0AuthContext) IsAccessTokenValidAndUserAuthorized(accessToken string) bool { 35 | _, ok := c.ValidAccessTokens[accessToken] 36 | 37 | if ok { 38 | return true 39 | } 40 | 41 | return false 42 | } 43 | 44 | func (c *Auth0AuthContext) GetUserName(accessToken string) string { 45 | username, _ := c.ValidAccessTokens[accessToken] 46 | return username 47 | } 48 | 49 | func (c *Auth0AuthContext) GetHTTPEndpointPrefix() string { 50 | return "/callback" 51 | } 52 | 53 | func (c *Auth0AuthContext) GetCookieName() string { 54 | return "auth0_token" 55 | } 56 | 57 | func (c *Auth0AuthContext) ServeHTTP(w http.ResponseWriter, req *http.Request) { 58 | conf := &oauth2.Config{ 59 | ClientID: c.ClientID, 60 | ClientSecret: c.ClientSecret, 61 | RedirectURL: c.CallbackURL, 62 | Scopes: []string{"openid", "profile"}, 63 | Endpoint: oauth2.Endpoint{ 64 | AuthURL: fmt.Sprintf("https://%s/authorize", c.AuthDomain), 65 | TokenURL: fmt.Sprintf("https://%s/oauth/token", c.AuthDomain), 66 | }, 67 | } 68 | 69 | code := req.URL.Query().Get("code") 70 | 71 | token, err := conf.Exchange(oauth2.NoContext, code) 72 | 73 | if err != nil { 74 | http.Error(w, err.Error(), http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | accessToken := token.AccessToken 79 | 80 | expiration := time.Now().Add(24 * time.Hour) 81 | cookie := http.Cookie{ 82 | Name: c.GetCookieName(), 83 | Value: accessToken, 84 | Expires: expiration, 85 | } 86 | 87 | http.SetCookie(w, &cookie) 88 | 89 | client := conf.Client(oauth2.NoContext, token) 90 | resp, err := client.Get(fmt.Sprintf("https://%s/userinfo", c.AuthDomain)) 91 | if err != nil { 92 | http.Error(w, err.Error(), http.StatusInternalServerError) 93 | return 94 | } 95 | 96 | raw, err := ioutil.ReadAll(resp.Body) 97 | defer resp.Body.Close() 98 | if err != nil { 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | 103 | var profile map[string]interface{} 104 | if err = json.Unmarshal(raw, &profile); err != nil { 105 | http.Error(w, err.Error(), http.StatusInternalServerError) 106 | return 107 | } 108 | 109 | var username string 110 | username = profile["email"].(string) 111 | 112 | usernames := MapUserNames(c.Config.Users, func(user interface{}) string { 113 | return user.(User).Username 114 | }) 115 | 116 | fmt.Println(username) 117 | fmt.Println(usernames) 118 | 119 | if inArray(username, usernames) { 120 | c.ValidAccessTokens[accessToken] = username 121 | } 122 | 123 | http.Redirect(w, req, "/", 302) 124 | } 125 | 126 | func (c *Auth0AuthContext) GetLoginPage() ([]byte, error) { 127 | return c.HTMLFile, nil 128 | } 129 | 130 | func (c *Auth0AuthContext) RenderHTMLFile() error { 131 | tplBytes, err := publicAuth0HtmlTplBytes() 132 | if err != nil { 133 | return err 134 | } 135 | 136 | tpl := string(tplBytes) 137 | 138 | f, err := RenderTemplate(tpl, c) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | c.HTMLFile = f 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /github_auth_context.go: -------------------------------------------------------------------------------- 1 | package authproxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type GithubAuthContext struct { 14 | ClientID string 15 | ClientSecret string 16 | Config *Configuration 17 | CookirName string 18 | ValidAccessTokens map[string]string 19 | HTMLFile []byte 20 | } 21 | 22 | type GithubAuthRequest struct { 23 | ClientID string `json:"client_id"` 24 | ClientSecret string `json:"client_secret"` 25 | Code string `json:"code"` 26 | } 27 | 28 | func NewGithubAuthContext(config *Configuration) *GithubAuthContext { 29 | return &GithubAuthContext{ 30 | ClientID: GetenvOrDie("CLIENT_ID"), 31 | ClientSecret: GetenvOrDie("CLIENT_SECRET"), 32 | Config: config, 33 | ValidAccessTokens: map[string]string{}, 34 | } 35 | } 36 | 37 | func (c *GithubAuthContext) GetLoginPage() ([]byte, error) { 38 | return c.HTMLFile, nil 39 | } 40 | 41 | func (c *GithubAuthContext) GetCookieName() string { 42 | return "github_token" 43 | } 44 | 45 | type mapf func(interface{}) string 46 | 47 | func MapUserNames(in []User, f mapf) []string { 48 | newArray := []string{} 49 | 50 | for _, v := range in { 51 | newArray = append(newArray, f(v)) 52 | } 53 | 54 | return newArray 55 | } 56 | 57 | type GithubUser struct { 58 | ID int `json:"id"` 59 | UserName string `json:"login"` 60 | } 61 | 62 | type GithubAuthResponse struct { 63 | AccessToken string `json:"access_token"` 64 | Scope string `json:"scope"` 65 | } 66 | 67 | func (c *GithubAuthContext) IsAccessTokenValidAndUserAuthorized(accessToken string) bool { 68 | _, ok := c.ValidAccessTokens[accessToken] 69 | 70 | if ok { 71 | return true 72 | } 73 | 74 | responseBytes, err := c.GetUserDetailsFromGithub(accessToken) 75 | 76 | if err != nil { 77 | return false 78 | } 79 | 80 | githubUser, err := c.ParseUserResponse(responseBytes) 81 | 82 | if err != nil { 83 | return false 84 | } 85 | 86 | usernames := MapUserNames(c.Config.Users, func(user interface{}) string { 87 | return user.(User).Username 88 | }) 89 | 90 | userExists := inArray(githubUser.UserName, usernames) 91 | 92 | if userExists { 93 | c.ValidAccessTokens[accessToken] = githubUser.UserName 94 | } 95 | 96 | return userExists 97 | } 98 | 99 | func (c *GithubAuthContext) GetUserName(accessToken string) string { 100 | username, _ := c.ValidAccessTokens[accessToken] 101 | return username 102 | } 103 | 104 | func (c *GithubAuthContext) ParseUserResponse(response []byte) (*GithubUser, error) { 105 | githubUser := &GithubUser{} 106 | err := json.Unmarshal(response, githubUser) 107 | 108 | return githubUser, err 109 | } 110 | 111 | func (c *GithubAuthContext) GetUserDetailsFromGithub(accessToken string) ([]byte, error) { 112 | client := &http.Client{} 113 | 114 | uri := fmt.Sprintf("https://api.github.com/user?access_token=%s", accessToken) 115 | req, err := http.NewRequest("GET", uri, nil) 116 | resp, err := client.Do(req) 117 | 118 | responseBody, err := ioutil.ReadAll(resp.Body) 119 | 120 | defer resp.Body.Close() 121 | 122 | return responseBody, err 123 | } 124 | 125 | func (c *GithubAuthContext) GetHTTPEndpointPrefix() string { 126 | return "/callback" 127 | } 128 | 129 | func (c *GithubAuthContext) ServeHTTP(w http.ResponseWriter, req *http.Request) { 130 | code := req.URL.Query().Get("code") 131 | client := &http.Client{} 132 | 133 | githubRequest := &GithubAuthRequest{ 134 | ClientID: c.ClientID, 135 | ClientSecret: c.ClientSecret, 136 | Code: code, 137 | } 138 | 139 | jsonRequestBody, _ := json.Marshal(githubRequest) 140 | 141 | req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonRequestBody)) 142 | 143 | if err != nil { 144 | fmt.Println(err) 145 | } 146 | 147 | req.Header.Set("Content-Type", "application/json") 148 | req.Header.Set("Accept", "application/json") 149 | 150 | resp, err := client.Do(req) 151 | 152 | responseBody, _ := ioutil.ReadAll(resp.Body) 153 | 154 | githubResponse := &GithubAuthResponse{} 155 | 156 | err = json.Unmarshal(responseBody, githubResponse) 157 | 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | 162 | expiration := time.Now().Add(24 * time.Hour) 163 | cookie := http.Cookie{ 164 | Name: c.GetCookieName(), 165 | Value: githubResponse.AccessToken, 166 | Expires: expiration, 167 | } 168 | 169 | http.SetCookie(w, &cookie) 170 | 171 | http.Redirect(w, req, "/", 302) 172 | } 173 | 174 | func (c *GithubAuthContext) RenderHTMLFile() error { 175 | tplBytes, err := publicGithubHtmlTplBytes() 176 | if err != nil { 177 | return err 178 | } 179 | 180 | tpl := string(tplBytes) 181 | 182 | f, err := RenderTemplate(tpl, c) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | c.HTMLFile = f 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-auth-proxy 2 | 3 | Transparent auth reverse proxy designed for micro-apps with static client and an API server. 4 | 5 | This project is built to serve **internal** projects at your company. Those can 6 | include dashboard, internal tools and others. 7 | 8 | It is built with ACL and Docker deployment in mind and fits a very broad use 9 | case that can really serve any company that requires authentication for 10 | internal tools without messing with auth for every single project. 11 | 12 | ## Overall architecture 13 | 14 |  15 | 16 | This is built to protext "upstreams" behind an auth backend (Currently only supports github). 17 | 18 | Say you have 2 upstreams configured: 19 | 20 | ``` 21 | // REDACTED 22 | "upstreams": [ 23 | { 24 | "type": "http", 25 | "prefix": "/", 26 | "location": "https://bdf682c0.ngrok.io" 27 | }, 28 | { 29 | "type": "http", 30 | "prefix": "/api/", 31 | "location": "https://6cc1b022.ngrok.io" 32 | } 33 | ] 34 | // REDACTED 35 | ``` 36 | 37 | Any request starting with `/api/` will be routed through to your backend server. Anything else `/` will go to the client. 38 | 39 | Because request are no longer routed directly from the client to the server, you will not have to define CORS or any sort of absolute URL on the client. You simply call `/api` if the client is behind the proxy and it will do the rest. 40 | 41 | ### Features 42 | 43 | * Limiting users by username on Github 44 | * Restrict to a single http method. Some users will only be able to do GET and the rest can do anything. This comes in REALLY handy when you want to limit view vs edit without worrying about it on your backend. 45 | * Memorize the tokens, we don't DDOs gihub. Once a token has been verified it will be memorized and will not be checked. 46 | * Save the token in the cookie for the user (See limitations regarding this feature). 47 | * Docker friendly: Upstreams can be only visible to the main docker container and not accessible to the public. This makes the proxy the only gate to the code and you have to authenticate first. 48 | 49 | ## Configuration 50 | 51 | ### Config file 52 | 53 | ``` 54 | { 55 | "authContext": "github", 56 | "users": [ 57 | { 58 | "username": "KensoDev" 59 | }, 60 | { 61 | "username": "KensoDev2", 62 | "restrict": "GET" 63 | } 64 | ], 65 | "upstreams": [ 66 | { 67 | "type": "http", 68 | "prefix": "/", 69 | "location": "https://bdf682c0.ngrok.io" 70 | }, 71 | { 72 | "type": "http", 73 | "prefix": "/api/", 74 | "location": "https://6cc1b022.ngrok.io" 75 | } 76 | ] 77 | } 78 | 79 | ``` 80 | 81 | * `users` - Users that are allowed to access your app. 82 | * `upstreams` - HTTP supported upstreams that the proxy will call 83 | * `authContext` - Authentication context you want to use. Currently only 84 | supports `github`, see design docs for the interface implemented. 85 | 86 | ### Env Vars 87 | 88 | ### For Github 89 | 90 | ENV vars you'll need to config in order for the proxy to work 91 | 92 | ``` 93 | CLIENT_ID 94 | CLIENT_SECRET 95 | ``` 96 | 97 | ### For Auth0 98 | 99 | // TODO 100 | 101 | These are your Github Client ID and Client secret from the Oauth page. 102 | 103 | ## Usage 104 | 105 | ``` 106 | usage: authproxy --listen-port=LISTEN-PORT --config-location=CONFIG-LOCATION [