├── .gitignore ├── go.mod ├── go.sum ├── README.md ├── Makefile ├── LICENSE ├── api └── main.go ├── web └── main.go └── auth └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.pem 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/acoshift/example-cross-origin-auth-api 2 | 3 | go 1.12 4 | 5 | require github.com/dgrijalva/jwt-go v3.2.0+incompatible 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # example-cross-origin-auth-api 2 | 3 | Example Cross-Origin Auth and API Servers 4 | 5 | ## Steps 6 | 7 | 1. Generate RSA for sign JWT 8 | 9 | `make generate` 10 | 11 | 1. Start Auth Server 12 | 13 | `make start-auth` 14 | 15 | 1. Start API Server 16 | 17 | `make start-api` 18 | 19 | 1. Start Web Server 20 | 21 | `make start-web` 22 | 23 | 1. Browse web at http://localhost:8080 24 | 25 | 1. Sign In 26 | 27 | 1. You will sign in to localhost:8081, but profile api will called from localhost:8082 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo "Example Cross-Origin Auth and API Servers" 3 | @echo "Commands:" 4 | @echo "\tgenerate - generate RSA for signing" 5 | @echo "\tstart-auth - start auth server at origin http://localhost:8081" 6 | @echo "\tstart-api - start api server at origin http://localhost:8082" 7 | @echo "\tstart-web - start web server at origin http://localhost:8080" 8 | 9 | start-auth: 10 | go run ./auth 11 | 12 | start-api: 13 | go run ./api 14 | 15 | start-web: 16 | go run ./web 17 | 18 | generate: 19 | openssl genrsa -out private-key.pem 2048 20 | openssl rsa -in private-key.pem -pubout > public-key.pem 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thanatat Tamtan 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 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/dgrijalva/jwt-go" 14 | ) 15 | 16 | func main() { 17 | pubKey, _ := ioutil.ReadFile("public-key.pem") 18 | publicKey, err := jwt.ParseRSAPublicKeyFromPEM(pubKey) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | api := &api{ 24 | PublicKey: publicKey, 25 | } 26 | log.Println("API Server started at :8082") 27 | http.ListenAndServe(":8082", api) 28 | } 29 | 30 | type api struct { 31 | once sync.Once 32 | mux *http.ServeMux 33 | 34 | PublicKey *rsa.PublicKey 35 | } 36 | 37 | type errorResponse struct { 38 | Error string `json:"error"` 39 | } 40 | 41 | func (h *api) ServeHTTP(w http.ResponseWriter, r *http.Request) { 42 | h.once.Do(func() { 43 | h.mux = http.NewServeMux() 44 | h.mux.HandleFunc("/profile", h.profile) 45 | }) 46 | 47 | // always return json 48 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 49 | 50 | // general headers 51 | w.Header().Set("X-XSS-Protection", "1; mode=block") 52 | w.Header().Set("X-Frame-Options", "deny") 53 | w.Header().Set("X-Content-Type-Options", "nosniff") 54 | 55 | // start: CORS 56 | 57 | // allow all origins 58 | w.Header().Set("Access-Control-Allow-Origin", "*") 59 | // not allow credentials 60 | 61 | if r.Method == http.MethodOptions { 62 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE") 63 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 64 | w.Header().Set("Access-Control-Max-Age", "3600") 65 | w.Header().Set("Content-Type", "text/plain") 66 | 67 | // in-case we run behind cached reverse proxy 68 | w.Header().Add("Vary", "Origin") 69 | w.Header().Add("Vary", "Access-Control-Request-Method") 70 | w.Header().Add("Vary", "Access-Control-Request-Headers") 71 | 72 | w.WriteHeader(http.StatusNoContent) 73 | return 74 | } 75 | 76 | // end: CORS 77 | 78 | h.mux.ServeHTTP(w, r) 79 | } 80 | 81 | func (h *api) profile(w http.ResponseWriter, r *http.Request) { 82 | subject := h.parseToken(r) 83 | if subject == "" { 84 | w.WriteHeader(http.StatusUnauthorized) 85 | json.NewEncoder(w).Encode(errorResponse{"Unauthorized"}) 86 | return 87 | } 88 | 89 | json.NewEncoder(w).Encode(struct { 90 | ID string `json:"id"` 91 | Name string `json:"name"` 92 | }{subject, "Nakano Miku"}) 93 | } 94 | 95 | // parseToken parses token from request and return user's id 96 | func (h *api) parseToken(r *http.Request) string { 97 | auth := r.Header.Get("Authorization") 98 | if len(auth) < 7 { 99 | return "" 100 | } 101 | 102 | if !strings.EqualFold(auth[:7], "bearer ") { 103 | return "" 104 | } 105 | 106 | tokenStr := auth[7:] 107 | if tokenStr == "" { 108 | return "" 109 | } 110 | 111 | token, err := jwt.ParseWithClaims(tokenStr, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { 112 | if token.Method != jwt.SigningMethodRS256 { 113 | return nil, fmt.Errorf("invalid method") 114 | } 115 | return h.PublicKey, nil 116 | }) 117 | if err != nil { 118 | return "" 119 | } 120 | if !token.Valid { 121 | return "" 122 | } 123 | 124 | sub, _ := token.Claims.(jwt.MapClaims)["sub"].(string) 125 | return sub 126 | } 127 | -------------------------------------------------------------------------------- /web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | log.Println("Web Server started at :8080") 11 | http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | w.Header().Set("X-XSS-Protection", "1; mode=block") 13 | w.Header().Set("X-Frame-Options", "deny") 14 | w.Header().Set("X-Content-Type-Options", "nosniff") 15 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 16 | io.WriteString(w, index) 17 | })) 18 | } 19 | 20 | // language=HTML 21 | const index = ` 22 | 23 |