├── 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 | ![Auth proxy architecture](http://assets.avi.io/authproxy-diagram.png) 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 [] 107 | 108 | Flags: 109 | --help Show context-sensitive help (also try --help-long and --help-man). 110 | --listen=LISTEN-PORT Which port should the proxy listen on 111 | --config=CONFIG-LOCATION 112 | Proxy Config Location 113 | ``` 114 | 115 | ## Limitations 116 | 117 | * Currently, for `V1`, this only supports HTTP backend. So for example if you 118 | have a frontend and a backend, you can host your frontend on some service that 119 | support HTTP endpoint. eg: S3 with static hosting. Some `heroku` or anything 120 | that will give you a valid web address. 121 | * If you have a client side app calling to a server side through the proxy 122 | (which is what this is designed for), you will need to pass in the cookie 123 | for all server requests. 124 | 125 | If you are using `fetch`, you will need to use `{ credentials: "include" }`. 126 | This will make sure that the github token will get passed through to the 127 | proxy and it will direct the request to the server and not redirect to 128 | Github all over again. 129 | * From your client side, you can either call the PROXY url as the backend or simply use the prefix `/api` you defined in your configuration. 130 | 131 | 132 | ## Design Decisions and architecture 133 | 134 | ### The authentication context 135 | 136 | The authentication context is the class used in order to check whether a user 137 | is valid, whether the action they try to do is allowed or not. 138 | 139 | The interface supports a token and a username and returns simple objects such 140 | as strings and booleans. 141 | 142 | This was done by design in order to allow you to extend it and add many other 143 | contexts beyond Github such as Auth0, Google and so on. 144 | 145 | 146 | `IsAccessTokenValidAndUserAuthorized(accessToken string) bool` 147 | 148 | This method is used to validate the token and check whether the user is 149 | authorized. Checking whether they're included in the list of users or not 150 | 151 | 152 | `GetUserName(accessToken sting) string` 153 | 154 | Passing in an access token, this should return the username as a string 155 | 156 | 157 | `GetHTTPEndpointPrefix() string` 158 | 159 | What endpoint does the external service used to authentication needs exposing. 160 | 161 | For example, Github wants you to expose a `/callback` which they send a `code` 162 | to. You can read more about it [here in the docs.](https://developer.github.com/v3/guides/basics-of-authentication/) 163 | 164 | 165 | `ServeHTTP(w http.ResponseWriter, req *http.Request)` 166 | 167 | Your auth context needs to expose an HTTP handler in order to handle the 168 | callback from the service. 169 | 170 | 171 | 172 | ### Github Auth Context Token Memoization Process 173 | 174 | Once you have a Github Token in the http cookie, the proxy will validate that 175 | token against Github. Make sure the user is authorized (from the user list) and will not validate again. 176 | 177 | The cookie expiration is set for 24 hours, when you authenticate again, you will get revalidated. -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. 2 | // sources: 3 | // public/auth0.html.tpl 4 | // public/denied.html 5 | // public/github.html.tpl 6 | // DO NOT EDIT! 7 | 8 | package authproxy 9 | 10 | import ( 11 | "bytes" 12 | "compress/gzip" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "os" 17 | "path/filepath" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | func bindataRead(data []byte, name string) ([]byte, error) { 23 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 24 | if err != nil { 25 | return nil, fmt.Errorf("Read %q: %v", name, err) 26 | } 27 | 28 | var buf bytes.Buffer 29 | _, err = io.Copy(&buf, gz) 30 | clErr := gz.Close() 31 | 32 | if err != nil { 33 | return nil, fmt.Errorf("Read %q: %v", name, err) 34 | } 35 | if clErr != nil { 36 | return nil, err 37 | } 38 | 39 | return buf.Bytes(), nil 40 | } 41 | 42 | type asset struct { 43 | bytes []byte 44 | info os.FileInfo 45 | } 46 | 47 | type bindataFileInfo struct { 48 | name string 49 | size int64 50 | mode os.FileMode 51 | modTime time.Time 52 | } 53 | 54 | func (fi bindataFileInfo) Name() string { 55 | return fi.name 56 | } 57 | func (fi bindataFileInfo) Size() int64 { 58 | return fi.size 59 | } 60 | func (fi bindataFileInfo) Mode() os.FileMode { 61 | return fi.mode 62 | } 63 | func (fi bindataFileInfo) ModTime() time.Time { 64 | return fi.modTime 65 | } 66 | func (fi bindataFileInfo) IsDir() bool { 67 | return false 68 | } 69 | func (fi bindataFileInfo) Sys() interface{} { 70 | return nil 71 | } 72 | 73 | var _publicAuth0HtmlTpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x8f\x41\x6b\xc3\x30\x0c\x85\xef\xfb\x15\x5a\x0e\x3b\x46\x3b\x0f\x27\x63\x2c\x97\x42\x4e\x85\xfe\x00\xc5\x71\x63\x53\xc7\x36\xa9\x02\x0d\xc6\xff\xbd\x38\x86\xb6\xf4\xa4\xef\x21\x3d\xf1\x9e\xd0\x3c\xdb\xf6\x03\x40\x68\x45\xe3\x0e\xf8\xa0\xc1\x8f\x5b\x06\x00\x11\xca\x04\xf8\x5b\x59\x7f\x03\xad\xac\x95\x63\x23\x89\x8d\x77\x10\x16\x7f\xdb\xca\x21\x86\x77\x87\x20\xd0\x8b\x3a\x37\x95\x66\x0e\xd7\x1f\xc4\x18\xeb\xfc\xa5\xf3\x33\x19\x97\x12\x5a\x3f\x19\xf7\x2b\xad\x51\x8e\x9b\x18\xeb\xff\x9d\x0e\x5d\x4a\x5f\x92\xac\x1d\x48\x5e\x4e\xc7\x7e\xdf\x3c\x65\x4a\x55\xdb\x67\xa3\x40\x6a\x81\x3d\x0c\x6a\x32\xee\x33\xab\xd7\x20\x02\x4b\x09\x81\xa5\xe8\x3d\x00\x00\xff\xff\x8d\xc1\xc2\xee\xf0\x00\x00\x00") 74 | 75 | func publicAuth0HtmlTplBytes() ([]byte, error) { 76 | return bindataRead( 77 | _publicAuth0HtmlTpl, 78 | "public/auth0.html.tpl", 79 | ) 80 | } 81 | 82 | func publicAuth0HtmlTpl() (*asset, error) { 83 | bytes, err := publicAuth0HtmlTplBytes() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | info := bindataFileInfo{name: "public/auth0.html.tpl", size: 240, mode: os.FileMode(420), modTime: time.Unix(1503085967, 0)} 89 | a := &asset{bytes: bytes, info: info} 90 | return a, nil 91 | } 92 | 93 | var _publicDeniedHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\xce\x41\x0a\x02\x31\x0c\x85\xe1\xbd\xa7\x78\x07\x10\xe6\x02\xc5\x7b\xb8\xcc\x4c\x1f\xb4\xe0\x24\x43\x93\x2e\xea\xe9\x45\x03\xae\xf2\xad\xf2\xbf\xd2\xe2\x7c\x3d\x6e\x40\x69\x94\xfa\xc3\xf6\xd7\x6e\x75\x7d\x01\x94\x2b\x2f\xf0\xb4\x39\x20\xc7\x41\x77\x34\x71\xec\xa4\xa2\x52\x3b\xeb\x1d\xcb\x26\x64\x10\x6a\x01\x51\xc8\x8c\x66\xa3\xbf\x59\x31\x9d\x03\x5d\x11\xad\x3b\x7c\x79\xf0\xcc\xc7\xdb\x95\xd1\x6c\x95\x2d\xf7\x7c\x02\x00\x00\xff\xff\x26\x40\x3a\x44\x97\x00\x00\x00") 94 | 95 | func publicDeniedHtmlBytes() ([]byte, error) { 96 | return bindataRead( 97 | _publicDeniedHtml, 98 | "public/denied.html", 99 | ) 100 | } 101 | 102 | func publicDeniedHtml() (*asset, error) { 103 | bytes, err := publicDeniedHtmlBytes() 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | info := bindataFileInfo{name: "public/denied.html", size: 151, mode: os.FileMode(420), modTime: time.Unix(1501818654, 0)} 109 | a := &asset{bytes: bytes, info: info} 110 | return a, nil 111 | } 112 | 113 | var _publicGithubHtmlTpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x8f\xd1\x4a\xc6\x30\x0c\x85\xef\x7d\x8a\xf8\x5f\x78\xf9\xe7\x7e\x74\x13\x51\x10\xc1\x0b\xdf\x40\xba\x2e\xae\x81\xae\x29\x5d\x0a\xce\xb1\x77\x97\xae\x20\xf2\xdf\x24\xdf\x81\x9c\x70\x8e\xf1\xba\x84\xe1\x0e\xc0\x78\xb2\xd3\x09\xf8\x47\xa3\x4c\x5b\x05\x00\x93\xda\x06\x78\x65\xf5\x65\x84\xa7\xa2\x9e\xa2\xb2\xb3\xca\x12\xe1\x23\xcb\xf7\xd6\x2e\x31\xdd\x5a\x8c\x05\x9f\xe9\xab\xbf\x78\xd5\xb4\x76\x88\xf3\xf9\xe3\xea\x64\xc1\x20\x33\x47\x14\x5b\xd4\x63\x1d\x92\xf9\x87\x1e\x57\x27\x89\xfa\xb2\x52\xee\x68\xb1\x1c\x1e\x5c\x60\x8a\xfa\xc9\x53\xbf\xef\xd7\xe7\x53\xbc\xbd\x1c\xc7\x65\x78\xaf\x7e\x83\x76\x00\x15\x18\x69\xe6\x78\x5f\xd5\xff\x28\x06\x5b\x0f\x83\xad\xeb\x6f\x00\x00\x00\xff\xff\x6c\xc2\x83\x19\xf3\x00\x00\x00") 114 | 115 | func publicGithubHtmlTplBytes() ([]byte, error) { 116 | return bindataRead( 117 | _publicGithubHtmlTpl, 118 | "public/github.html.tpl", 119 | ) 120 | } 121 | 122 | func publicGithubHtmlTpl() (*asset, error) { 123 | bytes, err := publicGithubHtmlTplBytes() 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | info := bindataFileInfo{name: "public/github.html.tpl", size: 243, mode: os.FileMode(420), modTime: time.Unix(1503085899, 0)} 129 | a := &asset{bytes: bytes, info: info} 130 | return a, nil 131 | } 132 | 133 | // Asset loads and returns the asset for the given name. 134 | // It returns an error if the asset could not be found or 135 | // could not be loaded. 136 | func Asset(name string) ([]byte, error) { 137 | cannonicalName := strings.Replace(name, "\\", "/", -1) 138 | if f, ok := _bindata[cannonicalName]; ok { 139 | a, err := f() 140 | if err != nil { 141 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 142 | } 143 | return a.bytes, nil 144 | } 145 | return nil, fmt.Errorf("Asset %s not found", name) 146 | } 147 | 148 | // MustAsset is like Asset but panics when Asset would return an error. 149 | // It simplifies safe initialization of global variables. 150 | func MustAsset(name string) []byte { 151 | a, err := Asset(name) 152 | if err != nil { 153 | panic("asset: Asset(" + name + "): " + err.Error()) 154 | } 155 | 156 | return a 157 | } 158 | 159 | // AssetInfo loads and returns the asset info for the given name. 160 | // It returns an error if the asset could not be found or 161 | // could not be loaded. 162 | func AssetInfo(name string) (os.FileInfo, error) { 163 | cannonicalName := strings.Replace(name, "\\", "/", -1) 164 | if f, ok := _bindata[cannonicalName]; ok { 165 | a, err := f() 166 | if err != nil { 167 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 168 | } 169 | return a.info, nil 170 | } 171 | return nil, fmt.Errorf("AssetInfo %s not found", name) 172 | } 173 | 174 | // AssetNames returns the names of the assets. 175 | func AssetNames() []string { 176 | names := make([]string, 0, len(_bindata)) 177 | for name := range _bindata { 178 | names = append(names, name) 179 | } 180 | return names 181 | } 182 | 183 | // _bindata is a table, holding each asset generator, mapped to its name. 184 | var _bindata = map[string]func() (*asset, error){ 185 | "public/auth0.html.tpl": publicAuth0HtmlTpl, 186 | "public/denied.html": publicDeniedHtml, 187 | "public/github.html.tpl": publicGithubHtmlTpl, 188 | } 189 | 190 | // AssetDir returns the file names below a certain 191 | // directory embedded in the file by go-bindata. 192 | // For example if you run go-bindata on data/... and data contains the 193 | // following hierarchy: 194 | // data/ 195 | // foo.txt 196 | // img/ 197 | // a.png 198 | // b.png 199 | // then AssetDir("data") would return []string{"foo.txt", "img"} 200 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 201 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 202 | // AssetDir("") will return []string{"data"}. 203 | func AssetDir(name string) ([]string, error) { 204 | node := _bintree 205 | if len(name) != 0 { 206 | cannonicalName := strings.Replace(name, "\\", "/", -1) 207 | pathList := strings.Split(cannonicalName, "/") 208 | for _, p := range pathList { 209 | node = node.Children[p] 210 | if node == nil { 211 | return nil, fmt.Errorf("Asset %s not found", name) 212 | } 213 | } 214 | } 215 | if node.Func != nil { 216 | return nil, fmt.Errorf("Asset %s not found", name) 217 | } 218 | rv := make([]string, 0, len(node.Children)) 219 | for childName := range node.Children { 220 | rv = append(rv, childName) 221 | } 222 | return rv, nil 223 | } 224 | 225 | type bintree struct { 226 | Func func() (*asset, error) 227 | Children map[string]*bintree 228 | } 229 | 230 | var _bintree = &bintree{nil, map[string]*bintree{ 231 | "public": &bintree{nil, map[string]*bintree{ 232 | "auth0.html.tpl": &bintree{publicAuth0HtmlTpl, map[string]*bintree{}}, 233 | "denied.html": &bintree{publicDeniedHtml, map[string]*bintree{}}, 234 | "github.html.tpl": &bintree{publicGithubHtmlTpl, map[string]*bintree{}}, 235 | }}, 236 | }} 237 | 238 | // RestoreAsset restores an asset under the given directory 239 | func RestoreAsset(dir, name string) error { 240 | data, err := Asset(name) 241 | if err != nil { 242 | return err 243 | } 244 | info, err := AssetInfo(name) 245 | if err != nil { 246 | return err 247 | } 248 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 249 | if err != nil { 250 | return err 251 | } 252 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 253 | if err != nil { 254 | return err 255 | } 256 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 257 | if err != nil { 258 | return err 259 | } 260 | return nil 261 | } 262 | 263 | // RestoreAssets restores an asset under the given directory recursively 264 | func RestoreAssets(dir, name string) error { 265 | children, err := AssetDir(name) 266 | // File 267 | if err != nil { 268 | return RestoreAsset(dir, name) 269 | } 270 | // Dir 271 | for _, child := range children { 272 | err = RestoreAssets(dir, filepath.Join(name, child)) 273 | if err != nil { 274 | return err 275 | } 276 | } 277 | return nil 278 | } 279 | 280 | func _filePath(dir, name string) string { 281 | cannonicalName := strings.Replace(name, "\\", "/", -1) 282 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 283 | } 284 | --------------------------------------------------------------------------------