├── .gitignore ├── Makefile ├── README.md ├── dotenv └── dotenv.go ├── main.go └── randstr └── randstr.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | /.idea 5 | /.env* 6 | 7 | /bin 8 | /node_modules 9 | /tmp 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET_BIN = netlify-cms-oauth-provider 2 | TARGET_ARCH = amd64 3 | SOURCE_MAIN = main.go 4 | LDFLAGS = -s -w 5 | 6 | all: build 7 | 8 | build: build-darwin build-linux build-windows 9 | 10 | build-darwin: 11 | CGO_ENABLED=0 GOOS=darwin GOARCH=$(TARGET_ARCH) go build -ldflags "$(LDFLAGS)" -o bin/$(TARGET_BIN)_darwin-amd64 $(SOURCE_MAIN) 12 | 13 | build-linux: 14 | CGO_ENABLED=0 GOOS=linux GOARCH=$(TARGET_ARCH) go build -ldflags "$(LDFLAGS)" -o bin/$(TARGET_BIN)_linux-amd64 $(SOURCE_MAIN) 15 | 16 | build-windows: 17 | CGO_ENABLED=0 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(LDFLAGS)" -o bin/$(TARGET_BIN)_windows-amd64.exe $(SOURCE_MAIN) 18 | 19 | start: 20 | go run $(SOURCE_MAIN) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netlify-cms-oauth-provider-go 2 | Netlify-CMS oauth client sending token in form as Netlify service itself, implementation in Go (golang) 3 | 4 | inspired by [netlify-cms-github-oauth-provider](https://github.com/vencax/netlify-cms-github-oauth-provider) (node-js). Thanks Václav! 5 | 6 | 7 | ## 1) Install 8 | 9 | TODO... Now just download binary release, or compile from source. 10 | 11 | ## 2) Config 12 | 13 | ### Auth Provider Config 14 | 15 | Configuration is done with environment variables, which can be supplied as command line arguments, added in your app hosting interface, or loaded from a .env ([dotenv](https://github.com/motdotla/dotenv)) file. 16 | 17 | **Example .env file:** 18 | 19 | ``` 20 | HOST=localhost:3000 21 | SESSION_SECRET=your-random-string 22 | GITHUB_KEY= 23 | GITHUB_SECRET= 24 | BITBUCKET_KEY= 25 | BITBUCKET_SECRET= 26 | GITLAB_KEY= 27 | GITLAB_SECRET= 28 | ``` 29 | 30 | **Client ID & Client Secret:** 31 | After registering your Oauth app, you will be able to get your client id and client secret on the next page. 32 | 33 | ### CMS Config 34 | You also need to add `base_url` to the backend section of your netlify-cms's config file. `base_url` is the live URL of this repo with no trailing slashes. 35 | 36 | ``` 37 | backend: 38 | name: github 39 | repo: user/repo # Path to your Github repository 40 | branch: master # Branch to update 41 | base_url: https://your.server.com # Path to ext auth provider 42 | ``` 43 | -------------------------------------------------------------------------------- /dotenv/dotenv.go: -------------------------------------------------------------------------------- 1 | package dotenv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Env defines Key and Value 12 | type Env struct { 13 | Key string 14 | Value string 15 | } 16 | 17 | var readFile = ioutil.ReadFile 18 | 19 | // File sets the env variables by a given file 20 | func File(filename string) error { 21 | envs, err := parseFile(filename) 22 | if err != nil { 23 | return err 24 | } 25 | err = setEnvs(envs) 26 | if err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // setEnvs takes []Env and sets all entries 33 | func setEnvs(envs []Env) error { 34 | for _, e := range envs { 35 | err := os.Setenv(e.Key, e.Value) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | // parseFile takes a filename as input and returns the values 44 | // as a slice. 45 | // The file should be in the form: 46 | // KEY1=VALUE1 47 | // KEY2=VALUE2 48 | // Spaces or empty lines are allowed. 49 | // Comments begin with # 50 | func parseFile(filename string) ([]Env, error) { 51 | var e []Env 52 | lines, err := loadLines(filename) 53 | if err != nil { 54 | return e, err 55 | } 56 | for _, l := range lines { 57 | env, err := parseLine(l) 58 | if err != nil { 59 | fmt.Println(err) 60 | continue 61 | } 62 | e = append(e, env) 63 | } 64 | return e, nil 65 | } 66 | 67 | func loadLines(filename string) ([]string, error) { 68 | var lines []string 69 | b, err := readFile(filename) 70 | if err != nil { 71 | return lines, err 72 | } 73 | splitLines := strings.Split(string(b), "\n") 74 | for _, l := range splitLines { 75 | if l == "" || strings.HasPrefix(l, "#") { 76 | continue 77 | } 78 | lines = append(lines, l) 79 | } 80 | return lines, err 81 | } 82 | 83 | func parseLine(line string) (Env, error) { 84 | var e Env 85 | s := strings.Split(line, "=") 86 | if len(s) != 2 { 87 | return Env{}, errors.New("No valid line!") 88 | } 89 | e.Key = strings.TrimSpace(s[0]) 90 | e.Value = strings.TrimSpace(s[1]) 91 | return e, nil 92 | } 93 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/gorilla/pat" 9 | "github.com/markbates/goth" 10 | "github.com/markbates/goth/gothic" 11 | "github.com/markbates/goth/providers/bitbucket" 12 | "github.com/markbates/goth/providers/github" 13 | "github.com/markbates/goth/providers/gitlab" 14 | 15 | "./dotenv" 16 | ) 17 | 18 | var ( 19 | host = "localhost:3000" 20 | ) 21 | 22 | const ( 23 | script = `
` 49 | ) 50 | 51 | // GET / 52 | func handleMain(res http.ResponseWriter, req *http.Request) { 53 | res.Header().Set("Content-Type", "text/html; charset=utf-8") 54 | res.WriteHeader(http.StatusOK) 55 | res.Write([]byte(``)) 56 | } 57 | 58 | // GET /auth Page redirecting after provider get param 59 | func handleAuth(res http.ResponseWriter, req *http.Request) { 60 | url := fmt.Sprintf("%s/auth/%s", host, req.FormValue("provider")) 61 | fmt.Printf("redirect to %s\n", url) 62 | http.Redirect(res, req, url, http.StatusTemporaryRedirect) 63 | } 64 | 65 | // GET /auth/provider Initial page redirecting by provider 66 | func handleAuthProvider(res http.ResponseWriter, req *http.Request) { 67 | gothic.BeginAuthHandler(res, req) 68 | } 69 | 70 | // GET /callback/{provider} Called by provider after authorization is granted 71 | func handleCallbackProvider(res http.ResponseWriter, req *http.Request) { 72 | var ( 73 | status string 74 | result string 75 | ) 76 | provider, errProvider := gothic.GetProviderName(req) 77 | user, errAuth := gothic.CompleteUserAuth(res, req) 78 | status = "error" 79 | if errProvider != nil { 80 | fmt.Printf("provider failed with '%s'\n", errProvider) 81 | result = fmt.Sprintf("%s", errProvider) 82 | } else if errAuth != nil { 83 | fmt.Printf("auth failed with '%s'\n", errAuth) 84 | result = fmt.Sprintf("%s", errAuth) 85 | } else { 86 | fmt.Printf("Logged in as %s user: %s (%s)\n", user.Provider, user.Email, user.AccessToken) 87 | status = "success" 88 | result = fmt.Sprintf(`{"token":"%s", "provider":"%s"}`, user.AccessToken, user.Provider) 89 | } 90 | res.Header().Set("Content-Type", "text/html; charset=utf-8") 91 | res.WriteHeader(http.StatusOK) 92 | res.Write([]byte(fmt.Sprintf(script, status, provider, result))) 93 | } 94 | 95 | // GET /refresh 96 | func handleRefresh(res http.ResponseWriter, req *http.Request) { 97 | fmt.Printf("refresh with '%s'\n", req) 98 | res.Write([]byte("")) 99 | } 100 | 101 | // GET /success 102 | func handleSuccess(res http.ResponseWriter, req *http.Request) { 103 | fmt.Printf("success with '%s'\n", req) 104 | res.Write([]byte("")) 105 | } 106 | 107 | func init() { 108 | dotenv.File(".env") 109 | if hostEnv, ok := os.LookupEnv("HOST"); ok { 110 | host = hostEnv 111 | } 112 | var ( 113 | gitlabProvider goth.Provider 114 | ) 115 | if gitlabServer, ok := os.LookupEnv("GITLAB_SERVER"); ok { 116 | gitlabProvider = gitlab.NewCustomisedURL( 117 | os.Getenv("GITLAB_KEY"), os.Getenv("GITLAB_SECRET"), 118 | fmt.Sprintf("https://%s/callback/gitlab", host), 119 | fmt.Sprintf("https://%s/oauth/authorize", gitlabServer), 120 | fmt.Sprintf("https://%s/oauth/token", gitlabServer), 121 | fmt.Sprintf("https://%s/api/v3/user", gitlabServer), 122 | ) 123 | } else { 124 | gitlabProvider = gitlab.New( 125 | os.Getenv("GITLAB_KEY"), os.Getenv("GITLAB_SECRET"), 126 | fmt.Sprintf("https://%s/callback/gitlab", host), 127 | ) 128 | } 129 | goth.UseProviders( 130 | github.New( 131 | os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), 132 | fmt.Sprintf("https://%s/callback/github", host), 133 | ), 134 | bitbucket.New( 135 | os.Getenv("BITBUCKET_KEY"), os.Getenv("BITBUCKET_SECRET"), 136 | fmt.Sprintf("https://%s/callback//bitbucket", host), 137 | ), 138 | gitlabProvider, 139 | ) 140 | } 141 | 142 | func main() { 143 | router := pat.New() 144 | router.Get("/callback/{provider}", handleCallbackProvider) 145 | router.Get("/auth/{provider}", handleAuthProvider) 146 | router.Get("/auth", handleAuth) 147 | router.Get("/refresh", handleRefresh) 148 | router.Get("/success", handleSuccess) 149 | router.Get("/", handleMain) 150 | // 151 | http.Handle("/", router) 152 | // 153 | fmt.Printf("Started running on %s\n", host) 154 | fmt.Println(http.ListenAndServe(host, nil)) 155 | } 156 | -------------------------------------------------------------------------------- /randstr/randstr.go: -------------------------------------------------------------------------------- 1 | package randstr 2 | 3 | import ( 4 | cryptrand "crypto/rand" 5 | "encoding/hex" 6 | mathrand "math/rand" 7 | "time" 8 | ) 9 | 10 | func Byte(n int) []byte { 11 | b := make([]byte, n) 12 | _, err := cryptrand.Read(b) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return b 17 | } 18 | 19 | func RandomBytes(n int) []byte { 20 | return Byte(n) 21 | } 22 | 23 | func Base64(s int) string { 24 | return String(s, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/") 25 | } 26 | 27 | func Base62(s int) string { 28 | return String(s) 29 | } 30 | 31 | func Hex(s int) string { 32 | b := RandomBytes(s) 33 | hexstring := hex.EncodeToString(b) 34 | return hexstring 35 | } 36 | 37 | func RandomHex(s int) string { 38 | return Hex(s) 39 | } 40 | 41 | func String(s int, letters ...string) string { 42 | randomFactor := RandomBytes(1) 43 | mathrand.Seed(time.Now().UnixNano() * int64(randomFactor[0])) 44 | var letterRunes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 45 | if len(letters) > 0 { 46 | letterRunes = []rune(letters[0]) 47 | } 48 | b := make([]rune, s) 49 | for i := range b { 50 | b[i] = letterRunes[mathrand.Intn(len(letterRunes))] 51 | } 52 | return string(b) 53 | } 54 | 55 | func RandomString(s int, letters ...string) string { // s number of character 56 | return String(s, letters...) 57 | } 58 | --------------------------------------------------------------------------------