├── .gitignore ├── .dockerignore ├── test.sh ├── flowAuthCodeSignin.go ├── flowAuthCodeRandomString.go ├── go.mod ├── flowAuthCodePasswordRequest.go ├── flowAuthCodePasswordReset.go ├── flowAuthCodeSignup.go ├── main.go ├── LICENSE ├── .travis.yml ├── flow.go ├── Dockerfile ├── flowAuthCodeMain.go ├── flowAuthCodeVerify.go ├── flowAuthCodeSession.go ├── flowClientCredentials.go ├── flowAuthCodeFacebook.go ├── README.md ├── go.sum ├── sql.go ├── flowAuthCode.go └── postgrest-oauth-server.postman_collection.json /.gitignore: -------------------------------------------------------------------------------- 1 | postgrest-oauth-server 2 | postgrest-oauth-server-*-* 3 | vendor 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | postgrest-oauth-server 2 | postgrest-oauth-server-*-* 3 | vendor 4 | example 5 | .idea 6 | .git 7 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ./example 4 | docker-compose -f docker-compose.yml up -d --build 5 | cd ../ 6 | sleep 5 7 | newman run ./postgrest-oauth-server.postman_collection.json --bail --ignore-redirects --global-var host=localhost 8 | -------------------------------------------------------------------------------- /flowAuthCodeSignin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func init() { 9 | Router.HandleFunc("/signin", handlerSigninPost).Methods("POST") 10 | } 11 | 12 | func handlerSigninPost(w http.ResponseWriter, r *http.Request) { 13 | ClearSession(w) 14 | 15 | owner := Owner{ 16 | Username: r.FormValue("username"), 17 | Password: r.FormValue("password"), 18 | } 19 | 20 | if id, role, jti, err := owner.check(); err == nil { 21 | SetSession(id, role, jti, w) 22 | w.WriteHeader(http.StatusOK) 23 | } else { 24 | log.Printf(err.Error()) 25 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 26 | } 27 | 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /flowAuthCodeRandomString.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const ( 9 | letterBytes = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" 10 | numberBytes = "1234567890" 11 | ) 12 | 13 | func init() { 14 | rand.Seed(time.Now().UnixNano()) 15 | } 16 | 17 | func generateRandomString(length int) string { 18 | return generateRandom(letterBytes, length) 19 | } 20 | 21 | func generateRandomNumbers(length int) string { 22 | return generateRandom(numberBytes, length) 23 | } 24 | 25 | func generateRandom(template string, length int) string { 26 | result := make([]byte, length) 27 | 28 | for i := range result { 29 | result[i] = template[rand.Intn(len(template))] 30 | } 31 | 32 | return string(result) 33 | } 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/postgrest-oauth/api 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/caarlos0/env v3.5.0+incompatible 7 | github.com/danilopolani/gocialite v1.0.2 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/gorilla/handlers v1.4.2 11 | github.com/gorilla/mux v1.7.3 12 | github.com/gorilla/securecookie v1.1.1 13 | github.com/kr/pretty v0.1.0 // indirect 14 | github.com/lib/pq v1.2.0 15 | github.com/patrickmn/go-cache v2.1.0+incompatible 16 | github.com/rs/cors v1.7.0 17 | github.com/stretchr/testify v1.4.0 // indirect 18 | github.com/thedevsaddam/renderer v1.2.0 19 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect 20 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 21 | gopkg.in/oleiade/reflections.v1 v1.0.0 // indirect 22 | gopkg.in/yaml.v2 v2.2.7 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /flowAuthCodePasswordRequest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func init() { 10 | Router.HandleFunc("/password/request", handlerPassRequestPost).Methods("POST") 11 | } 12 | 13 | func handlerPassRequestPost(w http.ResponseWriter, r *http.Request) { 14 | ClearSession(w) 15 | code := generateRandomNumbers(flowConfig.VerificationCodeLength) 16 | 17 | owner := Owner{ 18 | Username: r.FormValue("username"), 19 | VerificationCode: code, 20 | VerificationRoute: authCodeConfig.OauthCodeUi + "/password/reset/" + code, 21 | } 22 | 23 | if id, err := owner.requestPassword(); id != "" && err == nil { 24 | log.Printf("password reset code for user '%s' is: %s", id, code) 25 | PassResetStorage.Set(code, id, cache.DefaultExpiration) 26 | w.WriteHeader(http.StatusOK) 27 | } else { 28 | log.Printf(err.Error()) 29 | Rnd.JSON(w, http.StatusNotFound, ErrorResponse{err.Error()}) 30 | } 31 | 32 | return 33 | 34 | } 35 | -------------------------------------------------------------------------------- /flowAuthCodePasswordReset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func init() { 9 | Router.HandleFunc("/password/reset", handlerPassResetPost).Methods("POST") 10 | } 11 | 12 | func handlerPassResetPost(w http.ResponseWriter, r *http.Request) { 13 | code := r.FormValue("code") 14 | password := r.FormValue("password") 15 | 16 | savedId, ok := PassResetStorage.Get(code) 17 | owner := &Owner{} 18 | 19 | if ok { 20 | owner.Id = savedId.(string) 21 | owner.Password = password 22 | if err := owner.resetPassword(); err == nil { 23 | PassResetStorage.Delete(code) 24 | log.Printf("password reset for user '%s' was successful", owner.Id) 25 | w.WriteHeader(http.StatusOK) 26 | } else { 27 | log.Printf(err.Error()) 28 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 29 | } 30 | } else { 31 | log.Printf("code '%s' is invalid", code) 32 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{"invalid code"}) 33 | } 34 | 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /flowAuthCodeSignup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func init() { 10 | Router.HandleFunc("/signup", handlerSignupPost).Methods("POST") 11 | } 12 | 13 | func handlerSignupPost(w http.ResponseWriter, r *http.Request) { 14 | code := generateRandomNumbers(flowConfig.VerificationCodeLength) 15 | 16 | owner := Owner{ 17 | Email: r.FormValue("email"), 18 | Phone: r.FormValue("phone"), 19 | Password: r.FormValue("password"), 20 | Data: r.FormValue("data"), 21 | VerificationCode: code, 22 | VerificationRoute: authCodeConfig.OauthCodeUi + "/verify/" + code, 23 | } 24 | 25 | if id, err := owner.create(); err == nil { 26 | VerifyStorage.Set(code, id, cache.DefaultExpiration) 27 | log.Printf("Verification code for user '%s' is: %s", id, code) 28 | w.WriteHeader(http.StatusCreated) 29 | } else { 30 | log.Printf(err.Error()) 31 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/caarlos0/env" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/gorilla/handlers" 10 | "github.com/gorilla/mux" 11 | "github.com/rs/cors" 12 | ) 13 | 14 | var Router = mux.NewRouter().StrictSlash(true) 15 | 16 | var authCodeConfig struct { 17 | ValidateRedirectURI bool `env:"OAUTH_VALIDATE_REDIRECT_URI" envDefault:"true"` 18 | OauthCodeUi string `env:"OAUTH_CODE_UI" envDefault:"http://localhost:3685"` 19 | AllowOrigin []string `env:"OAUTH_CORS_ALLOW_ORIGIN" envSeparator:"," envDefault:"http://localhost:3685,http://localhost:3001"` 20 | } 21 | 22 | func init() { 23 | err := env.Parse(&authCodeConfig) 24 | if err != nil { 25 | log.Printf("%+v\n", err) 26 | } 27 | } 28 | 29 | func main() { 30 | log.Println("Started!") 31 | corsRouter := cors.New(cors.Options{ 32 | AllowedOrigins: authCodeConfig.AllowOrigin, 33 | AllowCredentials: true, 34 | Debug: true, 35 | }).Handler(Router) 36 | loggedRouter := handlers.LoggingHandler(os.Stdout, corsRouter) 37 | log.Fatal(http.ListenAndServe(":3684", loggedRouter)) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivan Kuznetsov 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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | env: 4 | global: 5 | - POSTGRES_DB=test 6 | - POSTGRES_USER=user 7 | - POSTGRES_PASSWORD=pass 8 | - JWT_SECRET=seGHYFcret14khbg38%fhbg73345F56uyFVHfcsgjhjj2y53v87tv87&%C&c6vbbn 9 | - PGRST_SERVER_PROXY_URI=http://localhost:3000 10 | - PGRST_AUTHENTICATOR_PASSWORD=authpass 11 | - OAUTH_ACCESS_TOKEN_SECRET=morethan32symbolssecretkey!!!!!! 12 | - OAUTH_ACCESS_TOKEN_TTL=7200 13 | - OAUTH_REFRESH_TOKEN_SECRET=notlesshan32symbolssecretkey!!!! 14 | - OAUTH_COOKIE_HASH_KEY=supersecret 15 | - OAUTH_COOKIE_BLOCK_KEY=16charssecret!!! 16 | - OAUTH_VALIDATE_REDIRECT_URI=false 17 | 18 | before_install: 19 | 20 | - sudo apt-get update 21 | 22 | - sudo apt-get install docker-ce 23 | 24 | - sudo curl -L https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 25 | 26 | - sudo chmod +x /usr/local/bin/docker-compose 27 | 28 | - docker --version 29 | 30 | - docker-compose --version 31 | 32 | - wget -qO- https://deb.nodesource.com/setup_8.x | sudo bash - 33 | 34 | - sudo apt-get install nodejs 35 | 36 | - sudo npm install newman --global 37 | 38 | script: 39 | 40 | - ./test.sh 41 | 42 | -------------------------------------------------------------------------------- /flow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/caarlos0/env" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | type tokensResponse struct { 10 | AccessToken string `json:"access_token"` 11 | RefreshToken string `json:"refresh_token,omitempty"` 12 | TokenType string `json:"token_type"` 13 | } 14 | 15 | type errorResponse struct { 16 | Error string `json:"error"` 17 | ErrorDescription string `json:"error_description,omitempty"` 18 | State string `json:"state,omitempty"` 19 | } 20 | 21 | var flowConfig struct { 22 | AccessTokenSecret string `env:"OAUTH_ACCESS_TOKEN_SECRET" envDefault:"morethan32symbolssecretkey!!!!!!"` 23 | AccessTokenTTL int `env:"OAUTH_ACCESS_TOKEN_TTL" envDefault:"7200"` 24 | RefreshTokenSecret string `env:"OAUTH_REFRESH_TOKEN_SECRET" envDefault:"notlesshan32symbolssecretkey!!!!"` 25 | HasuraAllowedRoles []string `env:"HASURA_ALLOWED_ROLES" envSeparator:"," envDefault:""` 26 | VerificationCodeLength int `env:"VERIFICATION_CODE_LENGTH" envDefault:"9"` 27 | } 28 | 29 | func init() { 30 | err := env.Parse(&flowConfig) 31 | if err != nil { 32 | log.Printf("%+v\n", err) 33 | } 34 | 35 | } 36 | 37 | func jsonResponse(js []byte, w http.ResponseWriter, code int) { 38 | w.Header().Set("Content-Type", "application/json") 39 | w.WriteHeader(code) 40 | w.Write(js) 41 | } 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################ 2 | # STEP 1 build executable binary 3 | ############################ 4 | # golang alpine 1.12 5 | FROM golang@sha256:8cc1c0f534c0fef088f8fe09edc404f6ff4f729745b85deae5510bfd4c157fb2 as builder 6 | 7 | # Install git + SSL ca certificates. 8 | # Git is required for fetching the dependencies. 9 | # Ca-certificates is required to call HTTPS endpoints. 10 | RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates 11 | 12 | # Create appuser 13 | RUN adduser -D -g '' appuser 14 | WORKDIR $GOPATH/src/mypackage/myapp/ 15 | 16 | # use modules 17 | COPY go.mod . 18 | 19 | ENV GO111MODULE=on 20 | RUN go mod download 21 | 22 | COPY . . 23 | 24 | # Build the binary 25 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -a -installsuffix cgo -o /go/bin/app . 26 | 27 | ############################ 28 | # STEP 2 build a small image 29 | ############################ 30 | FROM scratch 31 | 32 | # Import from builder. 33 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 34 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 35 | COPY --from=builder /etc/passwd /etc/passwd 36 | 37 | # Copy our static executable 38 | COPY --from=builder /go/bin/app /go/bin/app 39 | 40 | # Use an unprivileged user. 41 | USER appuser 42 | 43 | # Run the app binary. 44 | ENTRYPOINT ["/go/bin/app"] 45 | -------------------------------------------------------------------------------- /flowAuthCodeMain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/danilopolani/gocialite" 6 | "github.com/patrickmn/go-cache" 7 | "github.com/thedevsaddam/renderer" 8 | "log" 9 | "net/http" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type ErrorResponse struct { 15 | Message string `json:"message"` 16 | } 17 | 18 | var gocial = gocialite.NewDispatcher() 19 | var VerifyStorage = cache.New(24*time.Hour, 2*time.Hour) 20 | var PassResetStorage = cache.New(10*time.Minute, 5*time.Minute) 21 | var Rnd = renderer.New() 22 | 23 | func init() { 24 | Router.HandleFunc("/logout", handlerLogout).Methods("GET") 25 | } 26 | 27 | func handlerLogout(w http.ResponseWriter, r *http.Request) { 28 | clientId := r.URL.Query().Get("client_id") 29 | redirectUriRequest := r.URL.Query().Get("redirect_uri") 30 | c := &Client{Id: clientId} 31 | err, redirectUri := c.check() 32 | 33 | if err != nil { 34 | log.Print(err) 35 | http.Error(w, err.Error(), http.StatusBadRequest) 36 | return 37 | } 38 | 39 | if authCodeConfig.ValidateRedirectURI == true { 40 | if len(redirectUriRequest) > 0 && !strings.HasPrefix(redirectUriRequest, redirectUri) { 41 | err = errors.New("access denied") 42 | log.Print(err) 43 | http.Error(w, err.Error(), http.StatusBadRequest) 44 | return 45 | } 46 | } 47 | 48 | if len(redirectUriRequest) > 0 { 49 | redirectUri = redirectUriRequest 50 | } 51 | 52 | ClearSession(w) 53 | 54 | http.Redirect(w, r, redirectUri, 302) 55 | } 56 | -------------------------------------------------------------------------------- /flowAuthCodeVerify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func init() { 10 | Router.HandleFunc("/verify", handlerVerifyPost).Methods("POST") 11 | Router.HandleFunc("/re-verify", handlerReVerifyPost).Methods("POST") 12 | } 13 | 14 | func handlerVerifyPost(w http.ResponseWriter, r *http.Request) { 15 | ClearSession(w) 16 | 17 | code := r.FormValue("code") 18 | 19 | savedId, ok := VerifyStorage.Get(code) 20 | owner := &Owner{} 21 | 22 | if ok { 23 | owner.Id = savedId.(string) 24 | if err := owner.verify(); ok && err == nil { 25 | VerifyStorage.Delete(code) 26 | log.Printf("user '%s' successfully verified", owner.Id) 27 | w.WriteHeader(http.StatusOK) 28 | } else { 29 | log.Print(err) 30 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 31 | } 32 | } else { 33 | log.Printf("code '%s' is invalid", code) 34 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{"invalid code"}) 35 | } 36 | 37 | return 38 | } 39 | 40 | func handlerReVerifyPost(w http.ResponseWriter, r *http.Request) { 41 | code := generateRandomNumbers(flowConfig.VerificationCodeLength) 42 | 43 | owner := Owner{ 44 | Username: r.FormValue("username"), 45 | VerificationCode: code, 46 | VerificationRoute: authCodeConfig.OauthCodeUi + "/verify/" + code, 47 | } 48 | 49 | if id, err := owner.reVerify(); err == nil { 50 | VerifyStorage.Set(code, id, cache.DefaultExpiration) 51 | log.Printf("Re-verification code for user '%s' is: %s", id, code) 52 | w.WriteHeader(http.StatusOK) 53 | } else { 54 | log.Printf(err.Error()) 55 | w.WriteHeader(http.StatusOK) 56 | } 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /flowAuthCodeSession.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/caarlos0/env" 5 | "github.com/gorilla/securecookie" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | var sessionConfig struct { 11 | CookieHashKey string `env:"OAUTH_COOKIE_HASH_KEY" envDefault:"supersecret"` 12 | CookieBlockKey string `env:"OAUTH_COOKIE_BLOCK_KEY" envDefault:"16charssecret!!!"` 13 | } 14 | var cookieHandler *securecookie.SecureCookie 15 | 16 | func init() { 17 | 18 | err := env.Parse(&sessionConfig) 19 | if err != nil { 20 | log.Printf("%+v\n", err) 21 | } 22 | 23 | cookieHandler = securecookie.New([]byte(sessionConfig.CookieHashKey), []byte(sessionConfig.CookieBlockKey)) 24 | 25 | blockKeyLength := len(sessionConfig.CookieBlockKey) 26 | if blockKeyLength != 16 && blockKeyLength != 24 && blockKeyLength != 32 { 27 | log.Panic("COOKIE_BLOCK_KEY length should be 16, 24 or 32!") 28 | } 29 | } 30 | 31 | func SetSession(id string, role string, jti string, response http.ResponseWriter) { 32 | value := map[string]string{"id": id, "role": role, "jti": jti} 33 | if encoded, err := cookieHandler.Encode("session", value); err == nil { 34 | cookie := &http.Cookie{ 35 | Name: "session", 36 | Value: encoded, 37 | Path: "/", 38 | } 39 | http.SetCookie(response, cookie) 40 | } else { 41 | log.Print("Session cookie error") 42 | } 43 | } 44 | 45 | func ClearSession(writer http.ResponseWriter) { 46 | cookie := &http.Cookie{ 47 | Name: "session", 48 | Value: "", 49 | Path: "/", 50 | MaxAge: -1, 51 | } 52 | http.SetCookie(writer, cookie) 53 | } 54 | 55 | func GetUser(request *http.Request) (userId string, userRole string, userJti string) { 56 | if cookie, err := request.Cookie("session"); err == nil { 57 | cookieValue := make(map[string]string) 58 | if err = cookieHandler.Decode("session", cookie.Value, &cookieValue); err == nil { 59 | userId = cookieValue["id"] 60 | userRole = cookieValue["role"] 61 | userJti = cookieValue["jti"] 62 | } 63 | } 64 | return userId, userRole, userJti 65 | } 66 | -------------------------------------------------------------------------------- /flowClientCredentials.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | type ClientCredentialsData struct { 12 | ClientId string 13 | ClientRole string 14 | ClientType string 15 | } 16 | 17 | func init() { 18 | Router.HandleFunc("/token", handlerClientCredentialsToken). 19 | Methods("POST"). 20 | MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { 21 | grantType := r.FormValue("grant_type") 22 | clientId := r.FormValue("client_id") 23 | clientSecret := r.FormValue("client_secret") 24 | if grantType == "client_credentials" && clientId != "" && clientSecret != "" { 25 | return true 26 | } else { 27 | return false 28 | } 29 | }) 30 | } 31 | 32 | func handlerClientCredentialsToken(w http.ResponseWriter, r *http.Request) { 33 | clientId := r.FormValue("client_id") 34 | clientSecret := r.FormValue("client_secret") 35 | 36 | c := Client{ 37 | Id: clientId, 38 | Secret: clientSecret, 39 | } 40 | 41 | err, ctype := c.check_secret() 42 | 43 | if err != nil { 44 | e := &errorResponse{Error: "invalid_grant"} 45 | js, _ := json.Marshal(e) 46 | jsonResponse(js, w, http.StatusBadRequest) 47 | return 48 | } else if ctype != "confidential" { 49 | e := &errorResponse{Error: "unauthorized_client"} 50 | js, _ := json.Marshal(e) 51 | jsonResponse(js, w, http.StatusBadRequest) 52 | return 53 | } else { 54 | data := ClientCredentialsData{ 55 | ClientRole: "msrv-" + clientId, 56 | ClientId: clientId, 57 | ClientType: ctype, 58 | } 59 | response := fillClientCredentialsResponse(data) 60 | js, _ := json.Marshal(response) 61 | jsonResponse(js, w, http.StatusOK) 62 | return 63 | } 64 | } 65 | 66 | func fillClientCredentialsResponse(data ClientCredentialsData) tokensResponse { 67 | accessToken := jwt.New(jwt.SigningMethodHS256) 68 | claims := accessToken.Claims.(jwt.MapClaims) 69 | claims["type"] = "access_token" 70 | claims["role"] = data.ClientRole 71 | claims["client_id"] = data.ClientId 72 | claims["client_type"] = data.ClientType 73 | accessTokenString, _ := accessToken.SignedString([]byte(flowConfig.AccessTokenSecret)) 74 | 75 | response := tokensResponse{ 76 | AccessToken: accessTokenString, 77 | TokenType: "bearer", 78 | } 79 | return response 80 | } 81 | -------------------------------------------------------------------------------- /flowAuthCodeFacebook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/caarlos0/env" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | var facebookConfig struct { 11 | ClientId string `env:"OAUTH_FACEBOOK_CLIENT_ID" envDefault:"supersecret"` 12 | ClientSecret string `env:"OAUTH_FACEBOOK_CLIENT_SECRET" envDefault:"16charssecret!!!"` 13 | } 14 | 15 | func init() { 16 | 17 | err := env.Parse(&facebookConfig) 18 | if err != nil { 19 | log.Printf("%+v\n", err) 20 | } 21 | 22 | Router.HandleFunc("/facebook/url", handlerFacebookUrl).Methods("GET") 23 | Router.HandleFunc("/facebook/enter", handlerFacebookEnter).Methods("POST") 24 | } 25 | 26 | func handlerFacebookUrl(w http.ResponseWriter, r *http.Request) { 27 | 28 | redirectUri := r.URL.Query().Get("redirect_uri") 29 | 30 | if redirectUri == "" { 31 | redirectUri = authCodeConfig.OauthCodeUi 32 | } 33 | 34 | authURL, err := gocial.New(). 35 | Driver("facebook"). // Set provider 36 | Scopes([]string{"email"}). // Set optional scope(s) 37 | Redirect( // 38 | facebookConfig.ClientId, // Client ID 39 | facebookConfig.ClientSecret, 40 | redirectUri, // Redirect URL 41 | ) 42 | 43 | // Check for errors (usually driver not valid) 44 | if err != nil { 45 | Rnd.JSON(w, http.StatusInternalServerError, ErrorResponse{err.Error()}) 46 | return 47 | } else { 48 | type Response struct { 49 | Url string `json:"url"` 50 | } 51 | js, _ := json.Marshal(Response{authURL}) 52 | jsonResponse(js, w, http.StatusOK) 53 | return 54 | } 55 | 56 | return 57 | } 58 | func handlerFacebookEnter(w http.ResponseWriter, r *http.Request) { 59 | 60 | code := r.FormValue("code") 61 | state := r.FormValue("state") 62 | 63 | // Handle callback and check for errors 64 | data, _, err := gocial.Handle(state, code) 65 | if err != nil { 66 | log.Printf(err.Error()) 67 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 68 | return 69 | } 70 | 71 | dataJson, err := json.Marshal(data) 72 | if err != nil { 73 | log.Printf(err.Error()) 74 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 75 | return 76 | } 77 | 78 | owner := Owner{FacebookId: data.ID, Data: string(dataJson)} 79 | 80 | if id, role, jti, err := owner.checkFacebook(); err == nil { 81 | 82 | if id != "" { 83 | SetSession(id, role, jti, w) 84 | w.WriteHeader(http.StatusOK) 85 | } else { 86 | if id, role, jti, err := owner.createFacebook(); err == nil { 87 | SetSession(id, role, jti, w) 88 | w.WriteHeader(http.StatusOK) 89 | } else { 90 | log.Printf(err.Error()) 91 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 92 | } 93 | } 94 | 95 | } else { 96 | log.Printf(err.Error()) 97 | Rnd.JSON(w, http.StatusForbidden, ErrorResponse{err.Error()}) 98 | } 99 | 100 | return 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README 2 | ------ 3 | 4 | [![Build Status](https://travis-ci.org/postgrest-oauth/api.svg?branch=master)](https://travis-ci.org/postgrest-oauth/api) 5 | 6 | Environment Variables 7 | ===================== 8 | 9 | **OAUTH_DB_CONN_STRING** 10 | 11 | Default: "postgres://user:pass@postgresql:5432/test?sslmode=disable" 12 | 13 | See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING for more information about connection string parameters. 14 | 15 | **OAUTH_ACCESS_TOKEN_JaW_SECRET** 16 | 17 | Default: "morethan32symbolssecretkey!!!!!!" 18 | 19 | Random string. Should be >= to 32 symbols. This is important. 20 | 21 | **OAUTH_ACCESS_TOKEN_TTL=7200** 22 | 23 | Default: 7200 24 | 25 | Access token life cycle in seconds 26 | 27 | **OAUTH_REFRESH_TOKEN_SECRET** 28 | 29 | Default: "notlesshan32symbolssecretkey!!!!" 30 | 31 | Random string. Should be >= to 32 symbols. This is important. 32 | 33 | **OAUTH_COOKIE_HASH_KEY** 34 | 35 | Default: "supersecret" 36 | 37 | Random string. 38 | 39 | **OAUTH_COOKIE_BLOCK_KEY** 40 | 41 | Default: "16charssecret!!!" 42 | 43 | Random string. Should be equal to 16, 24 or 32 symbols. This is important. 44 | 45 | 46 | **OAUTH_VALIDATE_REDIRECT_URI** 47 | 48 | Default: true 49 | 50 | This setting should be `true` when you use this in production. When set to `false` you can use any **redirect_uri**. Handy for development. 51 | 52 | **OAUTH_CODE_UI** 53 | 54 | Default: http://localhost:3685 55 | 56 | This is a URL of UI that is used for Authorization Code Flow. 57 | 58 | **OAUTH_CORS_ALLOW_ORIGIN** 59 | 60 | Default: http://localhost:3685,http://localhost:3001 61 | 62 | Allowed CORS origins 63 | 64 | **HASURA_ALLOWED_ROLES** 65 | 66 | Example: "editor,user" 67 | 68 | If specified, support for Hasura will be enabled and Hasura specific info will be added to the token: 69 | 70 | ``` 71 | "https://hasura.io/jwt/claims": { 72 | "x-hasura-allowed-roles": ["editor","user"], 73 | "x-hasura-default-role": "user", 74 | "x-hasura-user-id": "123" 75 | } 76 | ``` 77 | 78 | More info: https://hasura.io/docs/1.0/graphql/manual/auth/authentication/jwt.html 79 | 80 | Facebook Signup/Signin 81 | ====================== 82 | 83 | Prepare 84 | ------- 85 | 86 | 1. Go to [developers.facebook.com](https://developers.facebook.com) and create an app, add Facebook Login product ([tutorial](https://youtu.be/MpLCBEdhg3Y)) 87 | 2. Add OAUTH_FACEBOOK_CLIENT_ID and OAUTH_FACEBOOK_CLIENT_SECRET environmental variables 88 | 89 | Configure your app 90 | --------------- 91 | 92 | Add 2 functions to your database 93 | ```SQL 94 | CREATE OR REPLACE FUNCTION oauth2.create_facebook_owner(obj json, phone varchar, OUT id varchar, OUT role varchar, OUT jti varchar) 95 | AS $$ 96 | INSERT INTO api.users(email, phone, role, facebook_id) 97 | VALUES 98 | ( 99 | obj->>'email'::varchar, 100 | phone, 101 | 'verified', 102 | obj->>'id'::varchar 103 | ) 104 | RETURNING id::varchar, role::varchar, jti::varchar; 105 | $$ LANGUAGE SQL; 106 | 107 | CREATE OR REPLACE FUNCTION oauth2.check_owner_facebook(facebook_id varchar, OUT id varchar, OUT role varchar, OUT jti varchar) 108 | AS $$ 109 | SELECT id::varchar, role::varchar, jti::varchar FROM api.users 110 | WHERE facebook_id = check_owner_facebook.facebook_id; 111 | $$ LANGUAGE SQL; 112 | ``` 113 | 114 | Get facebook button URL 115 | ``` 116 | GET http://localhost:3684/facebook/url?redirect_uri=http://localhost:3685/ 117 | ``` 118 | 119 | After user clicks it he'll be returned to your app with `code` and `state`. Pass them to `/api/enter` route 120 | 121 | ``` 122 | POST http://localhost:3684/facebook/enter 123 | Content-Type: application/x-www-form-urlencoded 124 | 125 | code={CODE}&state={STATE} 126 | 127 | ``` 128 | 129 | If user don't exist it will be created. If it exists he'll be signed in. Now you can redirect your app to `/authorize` 130 | 131 | Testing with Newman 132 | =================== 133 | ``` 134 | $ cd ./example 135 | $ docker-compose -f docker-compose.yml up -d --build 136 | $ cd ../ 137 | $ newman run --bail --ignore-redirects --global-var host=localhost ./postgrest-oauth-server.postman_collection.json 138 | 139 | ``` 140 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= 4 | github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= 5 | github.com/danilopolani/gocialite v1.0.2 h1:VPyzljBB17rcxe+ARNCyID9Yb9NpvIDc8git6M3wk90= 6 | github.com/danilopolani/gocialite v1.0.2/go.mod h1:WyErrpglkCWi4+RGPZzBLjD0fK/M7Yo757lVTr6C8HA= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 13 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 16 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 17 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 18 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 19 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 20 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 21 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 22 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 25 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 26 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 27 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 28 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 29 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 33 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 36 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 37 | github.com/thedevsaddam/renderer v1.2.0 h1:+N0J8t/s2uU2RxX2sZqq5NbaQhjwBjfovMU28ifX2F4= 38 | github.com/thedevsaddam/renderer v1.2.0/go.mod h1:k/TdZXGcpCpHE/KNj//P2COcmYEfL8OV+IXDX0dvG+U= 39 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 40 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= 41 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 42 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 43 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 44 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 46 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 47 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 51 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/oleiade/reflections.v1 v1.0.0 h1:nV9NFaFd5bXKjilVvPvA+/V/tNQk1pOEEc9gGWDkj+s= 53 | gopkg.in/oleiade/reflections.v1 v1.0.0/go.mod h1:SpA8pv+LUnF0FbB2hyRxc8XSng78D6iLBZ11PDb8Z5g= 54 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 56 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 57 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | -------------------------------------------------------------------------------- /sql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/caarlos0/env" 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | type Owner struct { 13 | Id string 14 | FacebookId string 15 | Username string 16 | Password string 17 | Email string 18 | Phone string 19 | Data string 20 | VerificationCode string 21 | VerificationRoute string 22 | } 23 | 24 | var sqlConfig struct { 25 | DbConnString string `env:"OAUTH_DB_CONN_STRING" envDefault:"postgres://user:pass@localhost:5432/test?sslmode=disable"` 26 | } 27 | 28 | func init() { 29 | err := env.Parse(&sqlConfig) 30 | if err != nil { 31 | log.Printf("%+v\n", err) 32 | } 33 | } 34 | 35 | func (a *Owner) create() (id string, err error) { 36 | db, err := dbConnect() 37 | defer db.Close() 38 | query := fmt.Sprintf("SELECT id::varchar FROM oauth2.create_owner('%s', '%s', '%s', '%s', '%s', '%s')", 39 | a.Email, a.Phone, a.Password, a.Data, a.VerificationCode, a.VerificationRoute) 40 | err = db.QueryRow(query).Scan(&id) 41 | 42 | switch { 43 | case err != nil: 44 | log.Print(err) 45 | err = fmt.Errorf("looks like owner already exists") 46 | default: 47 | log.Printf("User created. ID: %s\n", id) 48 | } 49 | 50 | return id, err 51 | } 52 | 53 | func (a *Owner) createFacebook() (id string, role string, jti string, err error) { 54 | db, err := dbConnect() 55 | defer db.Close() 56 | query := fmt.Sprintf("SELECT id::varchar, role::varchar, jti::varchar FROM oauth2.create_facebook_owner('%s', '%s')", 57 | a.Data, a.Phone) 58 | var uId, uRole, uJti sql.NullString 59 | err = db.QueryRow(query).Scan(&uId, &uRole, &uJti) 60 | 61 | if err != nil { 62 | log.Print(err) 63 | err = fmt.Errorf("looks like facebook owner already exists") 64 | } else if uId.Valid && uRole.Valid && uJti.Valid { 65 | id, role, jti = uId.String, uRole.String, uJti.String 66 | log.Printf("Facebook user created. ID: %s\n", id) 67 | } else { 68 | err = fmt.Errorf("no 'id' or 'role' or 'jti'") 69 | } 70 | 71 | return id, role, jti, err 72 | } 73 | 74 | func (a *Owner) reVerify() (id string, err error) { 75 | db, err := dbConnect() 76 | defer db.Close() 77 | query := fmt.Sprintf("SELECT id::varchar FROM oauth2.re_verify('%s', '%s', '%s')", 78 | a.Username, a.VerificationCode, a.VerificationRoute) 79 | err = db.QueryRow(query).Scan(&id) 80 | 81 | switch { 82 | case err != nil: 83 | log.Print(err) 84 | err = fmt.Errorf("looks like owner '%s' doesn't exists or verified", a.Username) 85 | default: 86 | log.Printf("Verification code re-sent. ID: %s\n", id) 87 | } 88 | 89 | return id, err 90 | } 91 | 92 | func (a *Owner) check() (id string, role string, jti string, err error) { 93 | db, err := dbConnect() 94 | defer db.Close() 95 | 96 | query := fmt.Sprintf("SELECT id::varchar, role::varchar, jti::varchar FROM oauth2.check_owner('%s', '%s')", 97 | a.Username, a.Password) 98 | var uId, uRole, uJti sql.NullString 99 | err = db.QueryRow(query).Scan(&uId, &uRole, &uJti) 100 | 101 | if err != nil { 102 | log.Print(err) 103 | err = fmt.Errorf("something bad happened") 104 | } else if uId.Valid && uRole.Valid && uJti.Valid { 105 | id, role, jti = uId.String, uRole.String, uJti.String 106 | } else { 107 | err = fmt.Errorf("wrong login or password") 108 | } 109 | 110 | return id, role, jti, err 111 | } 112 | 113 | func (a *Owner) checkFacebook() (id string, role string, jti string, err error) { 114 | db, err := dbConnect() 115 | defer db.Close() 116 | 117 | query := fmt.Sprintf("SELECT id::varchar, role::varchar, jti::varchar FROM oauth2.check_owner_facebook('%s')", 118 | a.FacebookId) 119 | var uId, uRole, uJti sql.NullString 120 | err = db.QueryRow(query).Scan(&uId, &uRole, &uJti) 121 | 122 | if err != nil { 123 | log.Print(err) 124 | err = fmt.Errorf("something bad happened") 125 | } else if uId.Valid && uRole.Valid && uJti.Valid { 126 | id, role, jti = uId.String, uRole.String, uJti.String 127 | } 128 | 129 | return id, role, jti, err 130 | } 131 | 132 | func (a *Owner) verify() (resErr error) { 133 | db, err := dbConnect() 134 | defer db.Close() 135 | 136 | query := fmt.Sprintf("SELECT oauth2.verify_owner('%s')", 137 | a.Id) 138 | rows, err := db.Query(query) 139 | defer rows.Close() 140 | 141 | if err != nil { 142 | log.Print(err) 143 | err = fmt.Errorf("owner with id '%s' doesn't exist", a.Id) 144 | } 145 | 146 | resErr = err 147 | return resErr 148 | } 149 | 150 | func (a *Owner) requestPassword() (id string, resErr error) { 151 | db, err := dbConnect() 152 | defer db.Close() 153 | 154 | query := fmt.Sprintf("SELECT id::varchar FROM oauth2.password_request('%s', '%s', '%s')", 155 | a.Username, a.VerificationCode, a.VerificationRoute) 156 | err = db.QueryRow(query).Scan(&id) 157 | 158 | switch { 159 | case err != nil: 160 | log.Print(err) 161 | err = fmt.Errorf("looks like owner doesn't exist") 162 | default: 163 | log.Printf("User exist. ID: %s", id) 164 | } 165 | 166 | resErr = err 167 | return id, resErr 168 | } 169 | 170 | func (a *Owner) resetPassword() (resErr error) { 171 | db, err := dbConnect() 172 | defer db.Close() 173 | 174 | query := fmt.Sprintf("SELECT oauth2.password_reset('%s', '%s')", 175 | a.Id, a.Password) 176 | rows, err := db.Query(query) 177 | defer rows.Close() 178 | 179 | switch { 180 | case err != nil: 181 | log.Print(err) 182 | err = fmt.Errorf("password reset error. USER ID: '%s'", a.Id) 183 | default: 184 | log.Printf("password reseted. USER ID: %s", a.Id) 185 | } 186 | 187 | resErr = err 188 | return resErr 189 | } 190 | 191 | func (a *Owner) getOwnerRoleAndJtiById() (role string, jti string, err error) { 192 | db, err := dbConnect() 193 | defer db.Close() 194 | 195 | query := fmt.Sprintf("SELECT role::text, jti::text FROM oauth2.owner_role_and_jti_by_id('%s')", a.Id) 196 | var uRole, uJti sql.NullString 197 | err = db.QueryRow(query).Scan(&uRole, &uJti) 198 | 199 | if err != nil { 200 | log.Print(err) 201 | err = fmt.Errorf("something bad happened. Owner ID: '%s'", a.Id) 202 | } else if uRole.Valid && uJti.Valid { 203 | role = uRole.String 204 | jti = uJti.String 205 | } else { 206 | err = fmt.Errorf("wrong owner id '%s'", a.Id) 207 | } 208 | 209 | return role, jti, err 210 | } 211 | 212 | type Client struct { 213 | Id string 214 | Secret string 215 | } 216 | 217 | func (c *Client) check() (resErr error, redirectUri string) { 218 | db, err := dbConnect() 219 | defer db.Close() 220 | 221 | query := fmt.Sprintf("SELECT redirect_uri::text FROM oauth2.check_client('%s')", 222 | c.Id) 223 | var uRedirectUri sql.NullString 224 | err = db.QueryRow(query).Scan(&uRedirectUri) 225 | 226 | if err != nil { 227 | log.Print(err) 228 | err = fmt.Errorf("something bad happened. Client ID: '%s'", c.Id) 229 | } else if uRedirectUri.Valid { 230 | redirectUri = uRedirectUri.String 231 | } else { 232 | err = fmt.Errorf("wrong client id '%s'", c.Id) 233 | } 234 | 235 | resErr = err 236 | return resErr, redirectUri 237 | } 238 | 239 | func (c *Client) check_secret() (resErr error, ctype string) { 240 | db, err := dbConnect() 241 | defer db.Close() 242 | 243 | query := fmt.Sprintf("SELECT type::varchar FROM oauth2.check_client_secret('%s', '%s')", 244 | c.Id, c.Secret) 245 | var uType sql.NullString 246 | err = db.QueryRow(query).Scan(&uType) 247 | 248 | if err != nil { 249 | log.Print(err) 250 | err = fmt.Errorf("something bad happened. Client ID: '%s'", c.Id) 251 | } else if uType.Valid { 252 | ctype = uType.String 253 | } else { 254 | err = fmt.Errorf("wrong client id '%s'", c.Id) 255 | } 256 | 257 | resErr = err 258 | return resErr, ctype 259 | } 260 | 261 | func dbConnect() (*sql.DB, error) { 262 | return sql.Open("postgres", sqlConfig.DbConnString) 263 | } 264 | -------------------------------------------------------------------------------- /flowAuthCode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "fmt" 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/gorilla/mux" 13 | "github.com/patrickmn/go-cache" 14 | "net/url" 15 | "strings" 16 | ) 17 | 18 | type AuthCodeData struct { 19 | ClientId string 20 | ClientType string 21 | UserId string 22 | UserRole string 23 | UserJti string 24 | } 25 | 26 | type HasuraCodeData struct { 27 | UserId string `json:"x-hasura-user-id"` 28 | UserRole string `json:"x-hasura-default-role"` 29 | AllowedRoles []string `json:"x-hasura-allowed-roles"` 30 | } 31 | 32 | var Storage = cache.New(10*time.Minute, 20*time.Minute) 33 | 34 | func init() { 35 | Router.HandleFunc("/authorize", handlerAuthCode). 36 | Methods("GET"). 37 | Queries("response_type", "code", "client_id", "{client_id}") 38 | 39 | Router.HandleFunc("/token", handlerAuthCodeToken). 40 | Methods("POST"). 41 | MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { 42 | grantType := r.FormValue("grant_type") 43 | code := r.FormValue("code") 44 | clientId := r.FormValue("client_id") 45 | if grantType == "authorization_code" && code != "" && clientId != "" { 46 | return true 47 | } else { 48 | return false 49 | } 50 | }) 51 | 52 | Router.HandleFunc("/token", handlerAuthCodeRefreshToken). 53 | Methods("POST"). 54 | MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { 55 | grantType := r.FormValue("grant_type") 56 | refreshToken := r.FormValue("refresh_token") 57 | if grantType == "refresh_token" && refreshToken != "" { 58 | return true 59 | } else { 60 | return false 61 | } 62 | }) 63 | } 64 | 65 | func handlerAuthCode(w http.ResponseWriter, r *http.Request) { 66 | clientId := r.URL.Query().Get("client_id") 67 | redirectUriRequest := r.URL.Query().Get("redirect_uri") 68 | c := &Client{Id: clientId} 69 | err, redirectUri := c.check() 70 | 71 | if err != nil { 72 | log.Print(err) 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | 77 | if authCodeConfig.ValidateRedirectURI == true { 78 | if len(redirectUriRequest) > 0 && !strings.HasPrefix(redirectUriRequest, redirectUri) { 79 | err = errors.New("access denied") 80 | log.Print(err) 81 | http.Error(w, err.Error(), http.StatusBadRequest) 82 | return 83 | } 84 | } 85 | 86 | if len(redirectUriRequest) > 0 { 87 | redirectUri = redirectUriRequest 88 | } 89 | 90 | uId, uRole, uJti := GetUser(r) 91 | if uId == "" || uRole == "" { 92 | http.Redirect(w, r, authCodeConfig.OauthCodeUi+"/signin?"+r.URL.RawQuery, 302) 93 | return 94 | } 95 | 96 | code := generateRandomString(24) 97 | 98 | data := &AuthCodeData{ClientId: clientId, UserId: uId, UserRole: uRole, UserJti: uJti, ClientType: "public"} 99 | 100 | Storage.Set(code, *data, cache.DefaultExpiration) 101 | 102 | redirectUriParsed, _ := url.Parse(redirectUri) 103 | params, _ := url.ParseQuery(redirectUriParsed.RawQuery) 104 | 105 | if state := r.URL.Query().Get("state"); state != "" { 106 | params.Add("state", state) 107 | } 108 | 109 | redirectUriParsed.RawQuery = params.Encode() 110 | redirectString := redirectUriParsed.String() + "&code=" + code 111 | 112 | http.Redirect(w, r, redirectString, 302) 113 | return 114 | } 115 | 116 | func handlerAuthCodeToken(w http.ResponseWriter, r *http.Request) { 117 | code := r.FormValue("code") 118 | clientId := r.FormValue("client_id") 119 | 120 | if data, ok := Storage.Get(code); ok { 121 | if data.(AuthCodeData).ClientId == clientId { 122 | response := fillAuthFlowResponse(data.(AuthCodeData)) 123 | js, _ := json.Marshal(response) 124 | Storage.Delete(code) 125 | jsonResponse(js, w, http.StatusOK) 126 | return 127 | } else { 128 | err := errors.New("wrong client id") 129 | log.Print(err) 130 | http.Error(w, err.Error(), http.StatusBadRequest) 131 | return 132 | } 133 | } else { 134 | e := &errorResponse{Error: "invalid_grant"} 135 | js, _ := json.Marshal(e) 136 | 137 | jsonResponse(js, w, http.StatusBadRequest) 138 | return 139 | } 140 | } 141 | 142 | func handlerAuthCodeRefreshToken(w http.ResponseWriter, r *http.Request) { 143 | refreshTokenString := r.FormValue("refresh_token") 144 | refreshToken, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) { 145 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 146 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 147 | } 148 | return []byte(flowConfig.RefreshTokenSecret), nil 149 | }) 150 | 151 | if err != nil { 152 | log.Print(err) 153 | e := &errorResponse{Error: "invalid_grant", ErrorDescription: err.Error()} 154 | js, _ := json.Marshal(e) 155 | 156 | jsonResponse(js, w, http.StatusBadRequest) 157 | return 158 | } 159 | 160 | if claims, ok := refreshToken.Claims.(jwt.MapClaims); ok && refreshToken.Valid && claims["type"] == "refresh_token" { 161 | userId := claims["id"].(string) 162 | clientId := claims["client_id"].(string) 163 | claimsJti := claims["jti"].(string) 164 | claimsRole := claims["role"].(string) 165 | o := Owner{Id: userId} 166 | role, jti, err := o.getOwnerRoleAndJtiById() 167 | if err != nil { 168 | e := &errorResponse{Error: "invalid_grant"} 169 | js, _ := json.Marshal(e) 170 | jsonResponse(js, w, http.StatusBadRequest) 171 | return 172 | } 173 | if role != claimsRole { 174 | e := &errorResponse{Error: "invalid_grant"} 175 | js, _ := json.Marshal(e) 176 | jsonResponse(js, w, http.StatusBadRequest) 177 | return 178 | } 179 | if jti != claimsJti { 180 | e := &errorResponse{Error: "invalid_grant"} 181 | js, _ := json.Marshal(e) 182 | jsonResponse(js, w, http.StatusBadRequest) 183 | return 184 | } 185 | d := AuthCodeData{ 186 | UserId: userId, 187 | ClientId: clientId, 188 | UserRole: role, 189 | UserJti: jti, 190 | } 191 | 192 | response := fillAuthFlowResponse(d) 193 | js, _ := json.Marshal(response) 194 | jsonResponse(js, w, http.StatusOK) 195 | return 196 | } else { 197 | e := &errorResponse{Error: "invalid_grant", ErrorDescription: "token is invalid"} 198 | js, _ := json.Marshal(e) 199 | jsonResponse(js, w, http.StatusBadRequest) 200 | return 201 | } 202 | } 203 | 204 | func fillAuthFlowResponse(data AuthCodeData) tokensResponse { 205 | accessToken := jwt.New(jwt.SigningMethodHS256) 206 | claims := accessToken.Claims.(jwt.MapClaims) 207 | claims["type"] = "access_token" 208 | claims["role"] = data.UserRole 209 | claims["id"] = data.UserId 210 | claims["client_id"] = data.ClientId 211 | claims["client_type"] = data.ClientType 212 | claims["jti"] = data.UserJti 213 | claims["exp"] = time.Now().Add(time.Second * time.Duration(flowConfig.AccessTokenTTL)).Unix() 214 | 215 | if len(flowConfig.HasuraAllowedRoles) > 0 { 216 | HasuraData := HasuraCodeData{UserId: data.UserId, UserRole: data.UserRole, AllowedRoles: flowConfig.HasuraAllowedRoles} 217 | claims["https://hasura.io/jwt/claims"] = HasuraData 218 | } 219 | 220 | accessTokenString, _ := accessToken.SignedString([]byte(flowConfig.AccessTokenSecret)) 221 | 222 | refreshToken := jwt.New(jwt.SigningMethodHS256) 223 | claims = refreshToken.Claims.(jwt.MapClaims) 224 | claims["type"] = "refresh_token" 225 | claims["id"] = data.UserId 226 | claims["client_id"] = data.ClientId 227 | claims["client_type"] = data.ClientType 228 | claims["role"] = data.UserRole 229 | claims["jti"] = data.UserJti 230 | claims["exp"] = time.Now().Add(time.Hour * 24 * 365).Unix() 231 | refreshTokenString, _ := refreshToken.SignedString([]byte(flowConfig.RefreshTokenSecret)) 232 | 233 | response := tokensResponse{ 234 | AccessToken: accessTokenString, 235 | RefreshToken: refreshTokenString, 236 | TokenType: "bearer", 237 | } 238 | return response 239 | } 240 | -------------------------------------------------------------------------------- /postgrest-oauth-server.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "fcbefa6c-27ed-4d76-ba47-e537de278e1a", 4 | "name": "postgrest-oauth-server", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Authorization Code Flow", 10 | "item": [ 11 | { 12 | "name": "[Test] Code can be used once", 13 | "item": [ 14 | { 15 | "name": "Signup", 16 | "event": [ 17 | { 18 | "listen": "test", 19 | "script": { 20 | "id": "25e27108-02d4-42bd-90b8-2d9a8dbb5e67", 21 | "type": "text/javascript", 22 | "exec": [ 23 | "" 24 | ] 25 | } 26 | }, 27 | { 28 | "listen": "prerequest", 29 | "script": { 30 | "id": "481bab84-6556-4857-9a6d-08116b25c788", 31 | "type": "text/javascript", 32 | "exec": [ 33 | "var d = new Date().getTime();", 34 | "", 35 | "pm.globals.set(\"email\", d + \"@kek.kek\");", 36 | "pm.globals.set(\"phone\", d);", 37 | "" 38 | ] 39 | } 40 | } 41 | ], 42 | "request": { 43 | "method": "POST", 44 | "header": [ 45 | { 46 | "key": "Content-Type", 47 | "value": "application/x-www-form-urlencoded" 48 | } 49 | ], 50 | "body": { 51 | "mode": "urlencoded", 52 | "urlencoded": [ 53 | { 54 | "key": "email", 55 | "value": "{{email}}", 56 | "type": "text" 57 | }, 58 | { 59 | "key": "password", 60 | "value": "pass", 61 | "type": "text" 62 | }, 63 | { 64 | "key": "phone", 65 | "value": "{{phone}}", 66 | "type": "text" 67 | }, 68 | { 69 | "key": "data", 70 | "value": "en", 71 | "type": "text" 72 | } 73 | ] 74 | }, 75 | "url": { 76 | "raw": "http://{{host}}:3684/signup", 77 | "protocol": "http", 78 | "host": [ 79 | "{{host}}" 80 | ], 81 | "port": "3684", 82 | "path": [ 83 | "signup" 84 | ] 85 | } 86 | }, 87 | "response": [] 88 | }, 89 | { 90 | "name": "SignIn", 91 | "event": [ 92 | { 93 | "listen": "test", 94 | "script": { 95 | "id": "690949a8-bd82-476d-87e0-2098f8abed71", 96 | "type": "text/javascript", 97 | "exec": [ 98 | "" 99 | ] 100 | } 101 | } 102 | ], 103 | "request": { 104 | "method": "POST", 105 | "header": [ 106 | { 107 | "key": "Content-Type", 108 | "value": "application/x-www-form-urlencoded" 109 | } 110 | ], 111 | "body": { 112 | "mode": "urlencoded", 113 | "urlencoded": [ 114 | { 115 | "key": "username", 116 | "value": "{{email}}", 117 | "type": "text" 118 | }, 119 | { 120 | "key": "password", 121 | "value": "pass", 122 | "type": "text" 123 | } 124 | ] 125 | }, 126 | "url": { 127 | "raw": "http://{{host}}:3684/signin", 128 | "protocol": "http", 129 | "host": [ 130 | "{{host}}" 131 | ], 132 | "port": "3684", 133 | "path": [ 134 | "signin" 135 | ] 136 | } 137 | }, 138 | "response": [] 139 | }, 140 | { 141 | "name": "Get authorization code", 142 | "event": [ 143 | { 144 | "listen": "test", 145 | "script": { 146 | "id": "2a785d53-51d4-44e2-8f6d-1fd5f5b31203", 147 | "type": "text/javascript", 148 | "exec": [ 149 | "pm.test(\"Location is present\", function () {", 150 | " pm.response.to.have.header(\"Location\");", 151 | "});", 152 | "", 153 | "var redirectUri = postman.getResponseHeader(\"Location\");", 154 | "var matches = redirectUri.match(/code=([0-9a-zA-Z-]+)/);", 155 | "console.log(redirectUri);", 156 | "pm.globals.set(\"code\", matches[1]);" 157 | ] 158 | } 159 | } 160 | ], 161 | "request": { 162 | "method": "GET", 163 | "header": [], 164 | "body": { 165 | "mode": "raw", 166 | "raw": "" 167 | }, 168 | "url": { 169 | "raw": "http://{{host}}:3684/authorize?response_type=code&client_id=spa", 170 | "protocol": "http", 171 | "host": [ 172 | "{{host}}" 173 | ], 174 | "port": "3684", 175 | "path": [ 176 | "authorize" 177 | ], 178 | "query": [ 179 | { 180 | "key": "response_type", 181 | "value": "code" 182 | }, 183 | { 184 | "key": "client_id", 185 | "value": "spa" 186 | } 187 | ] 188 | } 189 | }, 190 | "response": [] 191 | }, 192 | { 193 | "name": "Use code", 194 | "event": [ 195 | { 196 | "listen": "test", 197 | "script": { 198 | "id": "51e006b9-6a4f-44ca-a05a-d1cac095a0c4", 199 | "type": "text/javascript", 200 | "exec": [ 201 | "" 202 | ] 203 | } 204 | } 205 | ], 206 | "request": { 207 | "method": "POST", 208 | "header": [ 209 | { 210 | "key": "Content-Type", 211 | "value": "application/x-www-form-urlencoded" 212 | } 213 | ], 214 | "body": { 215 | "mode": "urlencoded", 216 | "urlencoded": [ 217 | { 218 | "key": "grant_type", 219 | "value": "authorization_code", 220 | "type": "text" 221 | }, 222 | { 223 | "key": "code", 224 | "value": "{{code}}", 225 | "type": "text" 226 | }, 227 | { 228 | "key": "client_id", 229 | "value": "spa", 230 | "type": "text" 231 | } 232 | ] 233 | }, 234 | "url": { 235 | "raw": "http://{{host}}:3684/token", 236 | "protocol": "http", 237 | "host": [ 238 | "{{host}}" 239 | ], 240 | "port": "3684", 241 | "path": [ 242 | "token" 243 | ] 244 | } 245 | }, 246 | "response": [] 247 | }, 248 | { 249 | "name": "Use same code again", 250 | "event": [ 251 | { 252 | "listen": "test", 253 | "script": { 254 | "id": "17446786-0a29-4aa7-8cfa-b355c5e4e0c5", 255 | "type": "text/javascript", 256 | "exec": [ 257 | "pm.test(\"Status code is 400\", function () {", 258 | " pm.response.to.have.status(400);", 259 | "});", 260 | "", 261 | "pm.test(\"Error message is in place\", function () {", 262 | " var jsonData = pm.response.json();", 263 | " pm.expect(jsonData.error).to.eql(\"invalid_grant\");", 264 | "});" 265 | ] 266 | } 267 | } 268 | ], 269 | "request": { 270 | "method": "POST", 271 | "header": [ 272 | { 273 | "key": "Content-Type", 274 | "value": "application/x-www-form-urlencoded" 275 | } 276 | ], 277 | "body": { 278 | "mode": "urlencoded", 279 | "urlencoded": [ 280 | { 281 | "key": "grant_type", 282 | "value": "authorization_code", 283 | "type": "text" 284 | }, 285 | { 286 | "key": "code", 287 | "value": "{{code}}", 288 | "type": "text" 289 | }, 290 | { 291 | "key": "client_id", 292 | "value": "spa", 293 | "type": "text" 294 | } 295 | ] 296 | }, 297 | "url": { 298 | "raw": "http://{{host}}:3684/token", 299 | "protocol": "http", 300 | "host": [ 301 | "{{host}}" 302 | ], 303 | "port": "3684", 304 | "path": [ 305 | "token" 306 | ] 307 | } 308 | }, 309 | "response": [] 310 | } 311 | ], 312 | "_postman_isSubFolder": true 313 | }, 314 | { 315 | "name": "[Test] User already exist", 316 | "item": [ 317 | { 318 | "name": "Signup", 319 | "event": [ 320 | { 321 | "listen": "test", 322 | "script": { 323 | "id": "e4b5ee4c-641a-4dd0-86f4-a2edaccc0426", 324 | "type": "text/javascript", 325 | "exec": [ 326 | "pm.test(\"Successful POST request\", function () {", 327 | " pm.expect(pm.response.code).to.be.oneOf([201,202]);", 328 | "});" 329 | ] 330 | } 331 | }, 332 | { 333 | "listen": "prerequest", 334 | "script": { 335 | "id": "4e36e065-9a5b-4caf-bd86-ceb706bbab2a", 336 | "type": "text/javascript", 337 | "exec": [ 338 | "var d = new Date().getTime();", 339 | "", 340 | "pm.globals.set(\"email\", d + \"@kek.kek\");", 341 | "pm.globals.set(\"phone\", d);" 342 | ] 343 | } 344 | } 345 | ], 346 | "request": { 347 | "method": "POST", 348 | "header": [ 349 | { 350 | "key": "Content-Type", 351 | "value": "application/x-www-form-urlencoded" 352 | } 353 | ], 354 | "body": { 355 | "mode": "urlencoded", 356 | "urlencoded": [ 357 | { 358 | "key": "email", 359 | "value": "{{email}}", 360 | "type": "text" 361 | }, 362 | { 363 | "key": "password", 364 | "value": "pass", 365 | "type": "text" 366 | }, 367 | { 368 | "key": "phone", 369 | "value": "{{phone}}", 370 | "type": "text" 371 | }, 372 | { 373 | "key": "data", 374 | "value": "en", 375 | "type": "text" 376 | } 377 | ] 378 | }, 379 | "url": { 380 | "raw": "http://{{host}}:3684/signup", 381 | "protocol": "http", 382 | "host": [ 383 | "{{host}}" 384 | ], 385 | "port": "3684", 386 | "path": [ 387 | "signup" 388 | ] 389 | } 390 | }, 391 | "response": [] 392 | }, 393 | { 394 | "name": "Signup Again", 395 | "event": [ 396 | { 397 | "listen": "test", 398 | "script": { 399 | "id": "763d1411-677a-4945-9b5b-56a6b457160b", 400 | "type": "text/javascript", 401 | "exec": [ 402 | "pm.test(\"Status code is 403\", function () {", 403 | " pm.response.to.have.status(403);", 404 | "});" 405 | ] 406 | } 407 | }, 408 | { 409 | "listen": "prerequest", 410 | "script": { 411 | "id": "5b06e955-488c-41cb-993d-3e60dc05d39f", 412 | "type": "text/javascript", 413 | "exec": [ 414 | "" 415 | ] 416 | } 417 | } 418 | ], 419 | "request": { 420 | "method": "POST", 421 | "header": [ 422 | { 423 | "key": "Content-Type", 424 | "value": "application/x-www-form-urlencoded" 425 | } 426 | ], 427 | "body": { 428 | "mode": "urlencoded", 429 | "urlencoded": [ 430 | { 431 | "key": "email", 432 | "value": "{{email}}", 433 | "type": "text" 434 | }, 435 | { 436 | "key": "password", 437 | "value": "pass", 438 | "type": "text" 439 | }, 440 | { 441 | "key": "phone", 442 | "value": "{{phone}}", 443 | "type": "text" 444 | }, 445 | { 446 | "key": "data", 447 | "value": "en", 448 | "type": "text" 449 | } 450 | ] 451 | }, 452 | "url": { 453 | "raw": "http://{{host}}:3684/signup", 454 | "protocol": "http", 455 | "host": [ 456 | "{{host}}" 457 | ], 458 | "port": "3684", 459 | "path": [ 460 | "signup" 461 | ] 462 | } 463 | }, 464 | "response": [] 465 | } 466 | ], 467 | "_postman_isSubFolder": true 468 | }, 469 | { 470 | "name": "[Test] Signin gives cookie", 471 | "item": [ 472 | { 473 | "name": "Signup", 474 | "event": [ 475 | { 476 | "listen": "test", 477 | "script": { 478 | "id": "48de37c0-8c0f-498b-86be-7122f3855f3f", 479 | "type": "text/javascript", 480 | "exec": [ 481 | "pm.test(\"Successful POST request\", function () {", 482 | " pm.expect(pm.response.code).to.be.oneOf([201,202]);", 483 | "});" 484 | ] 485 | } 486 | }, 487 | { 488 | "listen": "prerequest", 489 | "script": { 490 | "id": "449b7a47-4d17-424e-9a86-83388ccd51af", 491 | "type": "text/javascript", 492 | "exec": [ 493 | "var d = new Date().getTime();", 494 | "", 495 | "pm.globals.set(\"email\", d + \"@kek.kek\");", 496 | "pm.globals.set(\"phone\", d);", 497 | "" 498 | ] 499 | } 500 | } 501 | ], 502 | "request": { 503 | "method": "POST", 504 | "header": [ 505 | { 506 | "key": "Content-Type", 507 | "value": "application/x-www-form-urlencoded" 508 | } 509 | ], 510 | "body": { 511 | "mode": "urlencoded", 512 | "urlencoded": [ 513 | { 514 | "key": "email", 515 | "value": "{{email}}", 516 | "type": "text" 517 | }, 518 | { 519 | "key": "password", 520 | "value": "pass", 521 | "type": "text" 522 | }, 523 | { 524 | "key": "phone", 525 | "value": "{{phone}}", 526 | "type": "text" 527 | }, 528 | { 529 | "key": "data", 530 | "value": "en", 531 | "type": "text" 532 | } 533 | ] 534 | }, 535 | "url": { 536 | "raw": "http://{{host}}:3684/signup", 537 | "protocol": "http", 538 | "host": [ 539 | "{{host}}" 540 | ], 541 | "port": "3684", 542 | "path": [ 543 | "signup" 544 | ] 545 | } 546 | }, 547 | "response": [] 548 | }, 549 | { 550 | "name": "Signin", 551 | "event": [ 552 | { 553 | "listen": "test", 554 | "script": { 555 | "id": "6a414a25-df7f-4ee4-bcf6-0cb033ee75b0", 556 | "type": "text/javascript", 557 | "exec": [ 558 | "pm.test(\"Cookie is returned\", function(){", 559 | " cookie = postman.getResponseCookie(\"session\").value;", 560 | " pm.expect(postman.getResponseCookie(\"session\").value).to.be.a(\"string\")", 561 | "});", 562 | "", 563 | "pm.test(\"Status code is 200\", function () {", 564 | " pm.response.to.have.status(200);", 565 | "});" 566 | ] 567 | } 568 | } 569 | ], 570 | "request": { 571 | "method": "POST", 572 | "header": [ 573 | { 574 | "key": "Content-Type", 575 | "value": "application/x-www-form-urlencoded" 576 | } 577 | ], 578 | "body": { 579 | "mode": "urlencoded", 580 | "urlencoded": [ 581 | { 582 | "key": "username", 583 | "value": "{{email}}", 584 | "type": "text" 585 | }, 586 | { 587 | "key": "password", 588 | "value": "pass", 589 | "type": "text" 590 | } 591 | ] 592 | }, 593 | "url": { 594 | "raw": "http://{{host}}:3684/signin", 595 | "protocol": "http", 596 | "host": [ 597 | "{{host}}" 598 | ], 599 | "port": "3684", 600 | "path": [ 601 | "signin" 602 | ] 603 | } 604 | }, 605 | "response": [] 606 | } 607 | ], 608 | "_postman_isSubFolder": true 609 | }, 610 | { 611 | "name": "[Test] User can get tokens", 612 | "item": [ 613 | { 614 | "name": "Signup", 615 | "event": [ 616 | { 617 | "listen": "test", 618 | "script": { 619 | "id": "34c99f48-61fc-4a31-9271-f37ef548ce06", 620 | "type": "text/javascript", 621 | "exec": [ 622 | "" 623 | ] 624 | } 625 | }, 626 | { 627 | "listen": "prerequest", 628 | "script": { 629 | "id": "a5b94b95-c10e-4c45-a8d0-439c24cbc429", 630 | "type": "text/javascript", 631 | "exec": [ 632 | "var d = new Date().getTime();", 633 | "", 634 | "pm.globals.set(\"email\", d + \"@kek.kek\");", 635 | "pm.globals.set(\"phone\", d);" 636 | ] 637 | } 638 | } 639 | ], 640 | "request": { 641 | "method": "POST", 642 | "header": [ 643 | { 644 | "key": "Content-Type", 645 | "value": "application/x-www-form-urlencoded" 646 | } 647 | ], 648 | "body": { 649 | "mode": "urlencoded", 650 | "urlencoded": [ 651 | { 652 | "key": "email", 653 | "value": "{{email}}", 654 | "type": "text" 655 | }, 656 | { 657 | "key": "password", 658 | "value": "pass", 659 | "type": "text" 660 | }, 661 | { 662 | "key": "phone", 663 | "value": "{{phone}}", 664 | "type": "text" 665 | }, 666 | { 667 | "key": "data", 668 | "value": "en", 669 | "type": "text" 670 | } 671 | ] 672 | }, 673 | "url": { 674 | "raw": "http://{{host}}:3684/signup", 675 | "protocol": "http", 676 | "host": [ 677 | "{{host}}" 678 | ], 679 | "port": "3684", 680 | "path": [ 681 | "signup" 682 | ] 683 | } 684 | }, 685 | "response": [] 686 | }, 687 | { 688 | "name": "Signin", 689 | "event": [ 690 | { 691 | "listen": "test", 692 | "script": { 693 | "id": "4517d470-7b41-4817-9c73-10105ce675b7", 694 | "type": "text/javascript", 695 | "exec": [ 696 | "" 697 | ] 698 | } 699 | } 700 | ], 701 | "request": { 702 | "method": "POST", 703 | "header": [ 704 | { 705 | "key": "Content-Type", 706 | "value": "application/x-www-form-urlencoded" 707 | } 708 | ], 709 | "body": { 710 | "mode": "urlencoded", 711 | "urlencoded": [ 712 | { 713 | "key": "username", 714 | "value": "{{email}}", 715 | "type": "text" 716 | }, 717 | { 718 | "key": "password", 719 | "value": "pass", 720 | "type": "text" 721 | } 722 | ] 723 | }, 724 | "url": { 725 | "raw": "http://{{host}}:3684/signin", 726 | "protocol": "http", 727 | "host": [ 728 | "{{host}}" 729 | ], 730 | "port": "3684", 731 | "path": [ 732 | "signin" 733 | ] 734 | } 735 | }, 736 | "response": [] 737 | }, 738 | { 739 | "name": "Get authorization code", 740 | "event": [ 741 | { 742 | "listen": "test", 743 | "script": { 744 | "id": "4b05350a-1c0f-4d5a-8f87-6a09c17b7fc3", 745 | "type": "text/javascript", 746 | "exec": [ 747 | "pm.test(\"Location is present\", function () {", 748 | " pm.response.to.have.header(\"Location\");", 749 | "});", 750 | "", 751 | "var redirectUri = postman.getResponseHeader(\"Location\");", 752 | "var matches = redirectUri.match(/code=([0-9a-zA-Z-]+)/);", 753 | "pm.globals.set(\"code\", matches[1]);" 754 | ] 755 | } 756 | } 757 | ], 758 | "request": { 759 | "method": "GET", 760 | "header": [], 761 | "body": { 762 | "mode": "raw", 763 | "raw": "" 764 | }, 765 | "url": { 766 | "raw": "http://{{host}}:3684/authorize?response_type=code&client_id=spa", 767 | "protocol": "http", 768 | "host": [ 769 | "{{host}}" 770 | ], 771 | "port": "3684", 772 | "path": [ 773 | "authorize" 774 | ], 775 | "query": [ 776 | { 777 | "key": "response_type", 778 | "value": "code" 779 | }, 780 | { 781 | "key": "client_id", 782 | "value": "spa" 783 | } 784 | ] 785 | } 786 | }, 787 | "response": [] 788 | }, 789 | { 790 | "name": "Get tokens", 791 | "event": [ 792 | { 793 | "listen": "test", 794 | "script": { 795 | "id": "26759d5e-777d-462f-b3f4-52dda46c15a9", 796 | "type": "text/javascript", 797 | "exec": [ 798 | "pm.test(\"Status code is 200\", function () {", 799 | " pm.response.to.have.status(200);", 800 | "});", 801 | "", 802 | "var schema = {", 803 | " \"access_token\": {", 804 | " \"type\": \"string\"", 805 | " },", 806 | " \"refresh_token\": {", 807 | " \"type\": \"string\"", 808 | " },", 809 | " \"token_type\": {", 810 | " \"type\": \"string\"", 811 | " }", 812 | "};", 813 | "", 814 | "pm.test('Schema is valid', function() {", 815 | " var jsonData = pm.response.json();", 816 | " pm.expect(tv4.validate(jsonData, schema)).to.be.true;", 817 | "});" 818 | ] 819 | } 820 | } 821 | ], 822 | "request": { 823 | "method": "POST", 824 | "header": [ 825 | { 826 | "key": "Content-Type", 827 | "value": "application/x-www-form-urlencoded" 828 | } 829 | ], 830 | "body": { 831 | "mode": "urlencoded", 832 | "urlencoded": [ 833 | { 834 | "key": "grant_type", 835 | "value": "authorization_code", 836 | "type": "text" 837 | }, 838 | { 839 | "key": "code", 840 | "value": "{{code}}", 841 | "type": "text" 842 | }, 843 | { 844 | "key": "client_id", 845 | "value": "spa", 846 | "type": "text" 847 | } 848 | ] 849 | }, 850 | "url": { 851 | "raw": "http://{{host}}:3684/token", 852 | "protocol": "http", 853 | "host": [ 854 | "{{host}}" 855 | ], 856 | "port": "3684", 857 | "path": [ 858 | "token" 859 | ] 860 | } 861 | }, 862 | "response": [] 863 | } 864 | ], 865 | "_postman_isSubFolder": true 866 | }, 867 | { 868 | "name": "[Test] Re-verification request", 869 | "item": [ 870 | { 871 | "name": "Signup", 872 | "event": [ 873 | { 874 | "listen": "test", 875 | "script": { 876 | "id": "34c99f48-61fc-4a31-9271-f37ef548ce06", 877 | "type": "text/javascript", 878 | "exec": [ 879 | "" 880 | ] 881 | } 882 | }, 883 | { 884 | "listen": "prerequest", 885 | "script": { 886 | "id": "95b6d468-a62e-41ae-a59d-9ffab35ba975", 887 | "type": "text/javascript", 888 | "exec": [ 889 | "var d = new Date().getTime();", 890 | "", 891 | "pm.globals.set(\"email\", d + \"@kek.kek\");", 892 | "pm.globals.set(\"phone\", d);" 893 | ] 894 | } 895 | } 896 | ], 897 | "request": { 898 | "method": "POST", 899 | "header": [ 900 | { 901 | "key": "Content-Type", 902 | "value": "application/x-www-form-urlencoded" 903 | } 904 | ], 905 | "body": { 906 | "mode": "urlencoded", 907 | "urlencoded": [ 908 | { 909 | "key": "email", 910 | "value": "{{email}}", 911 | "type": "text" 912 | }, 913 | { 914 | "key": "password", 915 | "value": "pass", 916 | "type": "text" 917 | }, 918 | { 919 | "key": "phone", 920 | "value": "{{phone}}", 921 | "type": "text" 922 | }, 923 | { 924 | "key": "data", 925 | "value": "en", 926 | "type": "text" 927 | } 928 | ] 929 | }, 930 | "url": { 931 | "raw": "http://{{host}}:3684/signup", 932 | "protocol": "http", 933 | "host": [ 934 | "{{host}}" 935 | ], 936 | "port": "3684", 937 | "path": [ 938 | "signup" 939 | ] 940 | } 941 | }, 942 | "response": [] 943 | }, 944 | { 945 | "name": "Re-verify by email", 946 | "event": [ 947 | { 948 | "listen": "test", 949 | "script": { 950 | "id": "f33dc29d-ba48-403f-99e0-57897a4865c9", 951 | "type": "text/javascript", 952 | "exec": [ 953 | "pm.test(\"Status code is 200\", function () {", 954 | " pm.response.to.have.status(200);", 955 | "});" 956 | ] 957 | } 958 | }, 959 | { 960 | "listen": "prerequest", 961 | "script": { 962 | "id": "169da7f4-c897-410b-960d-92db0c76945b", 963 | "type": "text/javascript", 964 | "exec": [ 965 | "" 966 | ] 967 | } 968 | } 969 | ], 970 | "request": { 971 | "method": "POST", 972 | "header": [ 973 | { 974 | "key": "Content-Type", 975 | "value": "application/x-www-form-urlencoded" 976 | } 977 | ], 978 | "body": { 979 | "mode": "urlencoded", 980 | "urlencoded": [ 981 | { 982 | "key": "username", 983 | "value": "{{email}}", 984 | "type": "text" 985 | } 986 | ] 987 | }, 988 | "url": { 989 | "raw": "http://{{host}}:3684/re-verify", 990 | "protocol": "http", 991 | "host": [ 992 | "{{host}}" 993 | ], 994 | "port": "3684", 995 | "path": [ 996 | "re-verify" 997 | ] 998 | } 999 | }, 1000 | "response": [] 1001 | }, 1002 | { 1003 | "name": "Re-verify should always return 200 OK", 1004 | "event": [ 1005 | { 1006 | "listen": "test", 1007 | "script": { 1008 | "id": "f33dc29d-ba48-403f-99e0-57897a4865c9", 1009 | "type": "text/javascript", 1010 | "exec": [ 1011 | "pm.test(\"Status code is 200\", function () {", 1012 | " pm.response.to.have.status(200);", 1013 | "});" 1014 | ] 1015 | } 1016 | }, 1017 | { 1018 | "listen": "prerequest", 1019 | "script": { 1020 | "id": "169da7f4-c897-410b-960d-92db0c76945b", 1021 | "type": "text/javascript", 1022 | "exec": [ 1023 | "" 1024 | ] 1025 | } 1026 | } 1027 | ], 1028 | "request": { 1029 | "method": "POST", 1030 | "header": [ 1031 | { 1032 | "key": "Content-Type", 1033 | "value": "application/x-www-form-urlencoded" 1034 | } 1035 | ], 1036 | "body": { 1037 | "mode": "urlencoded", 1038 | "urlencoded": [ 1039 | { 1040 | "key": "username", 1041 | "value": "jhvdjhisdgvc8t7sd8vt7s8dgvc", 1042 | "type": "text" 1043 | } 1044 | ] 1045 | }, 1046 | "url": { 1047 | "raw": "http://{{host}}:3684/re-verify", 1048 | "protocol": "http", 1049 | "host": [ 1050 | "{{host}}" 1051 | ], 1052 | "port": "3684", 1053 | "path": [ 1054 | "re-verify" 1055 | ] 1056 | } 1057 | }, 1058 | "response": [] 1059 | } 1060 | ], 1061 | "_postman_isSubFolder": true 1062 | } 1063 | ], 1064 | "event": [ 1065 | { 1066 | "listen": "prerequest", 1067 | "script": { 1068 | "id": "8471bf05-3d54-430b-afd9-42b99baf3010", 1069 | "type": "text/javascript", 1070 | "exec": [ 1071 | "" 1072 | ] 1073 | } 1074 | }, 1075 | { 1076 | "listen": "test", 1077 | "script": { 1078 | "id": "9ed81715-507f-41ce-806f-5d2876479f06", 1079 | "type": "text/javascript", 1080 | "exec": [ 1081 | "" 1082 | ] 1083 | } 1084 | } 1085 | ] 1086 | }, 1087 | { 1088 | "name": "Client Credentials Flow", 1089 | "item": [ 1090 | { 1091 | "name": "Access Token Request (No Secret)", 1092 | "event": [ 1093 | { 1094 | "listen": "test", 1095 | "script": { 1096 | "id": "c72fec52-afaa-43d9-a81a-fa51ccff574f", 1097 | "type": "text/javascript", 1098 | "exec": [ 1099 | "pm.test(\"Error message is in place\", function () {", 1100 | " var jsonData = pm.response.json();", 1101 | " pm.expect(jsonData.error).to.eql(\"invalid_grant\");", 1102 | "});", 1103 | "", 1104 | "pm.test(\"Status code is 400\", function () {", 1105 | " pm.response.to.have.status(400);", 1106 | "});" 1107 | ] 1108 | } 1109 | } 1110 | ], 1111 | "request": { 1112 | "method": "POST", 1113 | "header": [ 1114 | { 1115 | "key": "Content-Type", 1116 | "value": "application/x-www-form-urlencoded" 1117 | } 1118 | ], 1119 | "body": { 1120 | "mode": "urlencoded", 1121 | "urlencoded": [ 1122 | { 1123 | "key": "grant_type", 1124 | "value": "client_credentials", 1125 | "type": "text" 1126 | }, 1127 | { 1128 | "key": "client_id", 1129 | "value": "worker", 1130 | "type": "text" 1131 | }, 1132 | { 1133 | "key": "client_secret", 1134 | "value": "invalid secret", 1135 | "type": "text" 1136 | } 1137 | ] 1138 | }, 1139 | "url": { 1140 | "raw": "http://{{host}}:3684/token", 1141 | "protocol": "http", 1142 | "host": [ 1143 | "{{host}}" 1144 | ], 1145 | "port": "3684", 1146 | "path": [ 1147 | "token" 1148 | ] 1149 | } 1150 | }, 1151 | "response": [] 1152 | }, 1153 | { 1154 | "name": "Access Token Request", 1155 | "event": [ 1156 | { 1157 | "listen": "test", 1158 | "script": { 1159 | "id": "c57197d3-bf3d-41bc-8295-b3625a56cb3b", 1160 | "type": "text/javascript", 1161 | "exec": [ 1162 | "var data = JSON.parse(responseBody);", 1163 | "pm.globals.set(\"access_token\", data.access_token);", 1164 | "", 1165 | "pm.test(\"Status code is 200\", function () {", 1166 | " pm.response.to.have.status(200);", 1167 | "});", 1168 | "", 1169 | "var schema = {", 1170 | " \"access_token\": {", 1171 | " \"type\": \"string\"", 1172 | " },", 1173 | " \"token_type\": {", 1174 | " \"type\": \"string\"", 1175 | " }", 1176 | "};", 1177 | "", 1178 | "pm.test('Schema is valid', function() {", 1179 | " var jsonData = pm.response.json();", 1180 | " pm.expect(tv4.validate(jsonData, schema)).to.be.true;", 1181 | "});" 1182 | ] 1183 | } 1184 | } 1185 | ], 1186 | "request": { 1187 | "method": "POST", 1188 | "header": [ 1189 | { 1190 | "key": "Content-Type", 1191 | "value": "application/x-www-form-urlencoded" 1192 | } 1193 | ], 1194 | "body": { 1195 | "mode": "urlencoded", 1196 | "urlencoded": [ 1197 | { 1198 | "key": "grant_type", 1199 | "value": "client_credentials", 1200 | "type": "text" 1201 | }, 1202 | { 1203 | "key": "client_id", 1204 | "value": "worker", 1205 | "type": "text" 1206 | }, 1207 | { 1208 | "key": "client_secret", 1209 | "value": "secret", 1210 | "type": "text" 1211 | } 1212 | ] 1213 | }, 1214 | "url": { 1215 | "raw": "http://{{host}}:3684/token", 1216 | "protocol": "http", 1217 | "host": [ 1218 | "{{host}}" 1219 | ], 1220 | "port": "3684", 1221 | "path": [ 1222 | "token" 1223 | ] 1224 | } 1225 | }, 1226 | "response": [] 1227 | }, 1228 | { 1229 | "name": "Get client info", 1230 | "event": [ 1231 | { 1232 | "listen": "test", 1233 | "script": { 1234 | "type": "text/javascript", 1235 | "exec": [ 1236 | "tests[\"Status code is 200\"] = responseCode.code === 200;" 1237 | ] 1238 | } 1239 | } 1240 | ], 1241 | "request": { 1242 | "method": "GET", 1243 | "header": [ 1244 | { 1245 | "key": "Authorization", 1246 | "value": "Bearer {{access_token}}" 1247 | } 1248 | ], 1249 | "body": { 1250 | "mode": "raw", 1251 | "raw": "" 1252 | }, 1253 | "url": { 1254 | "raw": "http://{{host}}:3000/client", 1255 | "protocol": "http", 1256 | "host": [ 1257 | "{{host}}" 1258 | ], 1259 | "port": "3000", 1260 | "path": [ 1261 | "client" 1262 | ] 1263 | } 1264 | }, 1265 | "response": [] 1266 | } 1267 | ] 1268 | } 1269 | ] 1270 | } --------------------------------------------------------------------------------