github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/command/testdata/login-oauth-server/oauthserver.go (about) 1 // Package oauthserver is a very simplistic OAuth server used only for 2 // the testing of the "durgaform login" and "terraform logout" commands. 3 package oauthserver 4 5 import ( 6 "crypto/sha256" 7 "encoding/base64" 8 "fmt" 9 "html" 10 "log" 11 "net/http" 12 "net/url" 13 "strings" 14 ) 15 16 // Handler is an implementation of net/http.Handler that provides a stub 17 // OAuth server implementation with the following endpoints: 18 // 19 // /authz - authorization endpoint 20 // /token - token endpoint 21 // /revoke - token revocation (logout) endpoint 22 // 23 // The authorization endpoint returns HTML per normal OAuth conventions, but 24 // it also includes an HTTP header X-Redirect-To giving the same URL that the 25 // link in the HTML indicates, allowing a non-browser user-agent to traverse 26 // this robotically in automated tests. 27 var Handler http.Handler 28 29 type handler struct{} 30 31 func (h handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 32 switch req.URL.Path { 33 case "/authz": 34 h.serveAuthz(resp, req) 35 case "/token": 36 h.serveToken(resp, req) 37 case "/revoke": 38 h.serveRevoke(resp, req) 39 default: 40 resp.WriteHeader(404) 41 } 42 } 43 44 func (h handler) serveAuthz(resp http.ResponseWriter, req *http.Request) { 45 args := req.URL.Query() 46 if rt := args.Get("response_type"); rt != "code" { 47 resp.WriteHeader(400) 48 resp.Write([]byte("wrong response_type")) 49 log.Printf("/authz: incorrect response type %q", rt) 50 return 51 } 52 redirectURL, err := url.Parse(args.Get("redirect_uri")) 53 if err != nil { 54 resp.WriteHeader(400) 55 resp.Write([]byte(fmt.Sprintf("invalid redirect_uri %s: %s", args.Get("redirect_uri"), err))) 56 return 57 } 58 59 state := args.Get("state") 60 challenge := args.Get("code_challenge") 61 challengeMethod := args.Get("code_challenge_method") 62 if challengeMethod == "" { 63 challengeMethod = "plain" 64 } 65 66 // NOTE: This is not a suitable implementation for a real OAuth server 67 // because the code challenge is providing no security whatsoever. This 68 // is just a simple implementation for this stub server. 69 code := fmt.Sprintf("%s:%s", challengeMethod, challenge) 70 71 redirectQuery := redirectURL.Query() 72 redirectQuery.Set("code", code) 73 if state != "" { 74 redirectQuery.Set("state", state) 75 } 76 redirectURL.RawQuery = redirectQuery.Encode() 77 78 respBody := fmt.Sprintf(`<a href="%s">Log In and Consent</a>`, html.EscapeString(redirectURL.String())) 79 resp.Header().Set("Content-Type", "text/html") 80 resp.Header().Set("Content-Length", fmt.Sprintf("%d", len(respBody))) 81 resp.Header().Set("X-Redirect-To", redirectURL.String()) // For robotic clients, using webbrowser.MockLauncher 82 resp.WriteHeader(200) 83 resp.Write([]byte(respBody)) 84 } 85 86 func (h handler) serveToken(resp http.ResponseWriter, req *http.Request) { 87 if req.Method != "POST" { 88 resp.WriteHeader(405) 89 log.Printf("/token: unsupported request method %q", req.Method) 90 return 91 } 92 93 if err := req.ParseForm(); err != nil { 94 resp.WriteHeader(500) 95 log.Printf("/token: error parsing body: %s", err) 96 return 97 } 98 99 grantType := req.Form.Get("grant_type") 100 log.Printf("/token: grant_type is %q", grantType) 101 switch grantType { 102 103 case "authorization_code": 104 code := req.Form.Get("code") 105 codeParts := strings.SplitN(code, ":", 2) 106 if len(codeParts) != 2 { 107 log.Printf("/token: invalid code %q", code) 108 resp.Header().Set("Content-Type", "application/json") 109 resp.WriteHeader(400) 110 resp.Write([]byte(`{"error":"invalid_grant"}`)) 111 return 112 } 113 114 codeVerifier := req.Form.Get("code_verifier") 115 116 switch codeParts[0] { 117 case "plain": 118 if codeParts[1] != codeVerifier { 119 log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], codeVerifier) 120 resp.Header().Set("Content-Type", "application/json") 121 resp.WriteHeader(400) 122 resp.Write([]byte(`{"error":"invalid_grant"}`)) 123 return 124 } 125 case "S256": 126 h := sha256.New() 127 h.Write([]byte(codeVerifier)) 128 encVerifier := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 129 if codeParts[1] != encVerifier { 130 log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], encVerifier) 131 resp.Header().Set("Content-Type", "application/json") 132 resp.WriteHeader(400) 133 resp.Write([]byte(`{"error":"invalid_grant"}`)) 134 return 135 } 136 default: 137 log.Printf("/token: unsupported challenge method %q", codeParts[0]) 138 resp.Header().Set("Content-Type", "application/json") 139 resp.WriteHeader(400) 140 resp.Write([]byte(`{"error":"invalid_grant"}`)) 141 return 142 } 143 144 resp.Header().Set("Content-Type", "application/json") 145 resp.WriteHeader(200) 146 resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`)) 147 log.Println("/token: successful request") 148 149 case "password": 150 username := req.Form.Get("username") 151 password := req.Form.Get("password") 152 153 if username == "wrong" || password == "wrong" { 154 // These special "credentials" allow testing for the error case. 155 resp.Header().Set("Content-Type", "application/json") 156 resp.WriteHeader(400) 157 resp.Write([]byte(`{"error":"invalid_grant"}`)) 158 log.Println("/token: 'wrong' credentials") 159 return 160 } 161 162 resp.Header().Set("Content-Type", "application/json") 163 resp.WriteHeader(200) 164 resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`)) 165 log.Println("/token: successful request") 166 167 default: 168 resp.WriteHeader(400) 169 log.Printf("/token: unsupported grant type %q", grantType) 170 } 171 } 172 173 func (h handler) serveRevoke(resp http.ResponseWriter, req *http.Request) { 174 resp.WriteHeader(404) 175 } 176 177 func init() { 178 Handler = handler{} 179 }