// Package oauthserver is a very simplistic OAuth server used only for // the testing of the "terraform login" and "terraform logout" commands. package oauthserver import ( "crypto/sha256" "encoding/base64" "fmt" "html" "log" "net/http" "net/url" "strings" ) // Handler is an implementation of net/http.Handler that provides a stub // OAuth server implementation with the following endpoints: // // /authz - authorization endpoint // /token - token endpoint // /revoke - token revocation (logout) endpoint // // The authorization endpoint returns HTML per normal OAuth conventions, but // it also includes an HTTP header X-Redirect-To giving the same URL that the // link in the HTML indicates, allowing a non-browser user-agent to traverse // this robotically in automated tests. var Handler http.Handler type handler struct{} func (h handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/authz": h.serveAuthz(resp, req) case "/token": h.serveToken(resp, req) case "/revoke": h.serveRevoke(resp, req) default: resp.WriteHeader(404) } } func (h handler) serveAuthz(resp http.ResponseWriter, req *http.Request) { args := req.URL.Query() if rt := args.Get("response_type"); rt != "code" { resp.WriteHeader(400) resp.Write([]byte("wrong response_type")) log.Printf("/authz: incorrect response type %q", rt) return } redirectURL, err := url.Parse(args.Get("redirect_uri")) if err != nil { resp.WriteHeader(400) resp.Write([]byte(fmt.Sprintf("invalid redirect_uri %s: %s", args.Get("redirect_uri"), err))) return } state := args.Get("state") challenge := args.Get("code_challenge") challengeMethod := args.Get("code_challenge_method") if challengeMethod == "" { challengeMethod = "plain" } // NOTE: This is not a suitable implementation for a real OAuth server // because the code challenge is providing no security whatsoever. This // is just a simple implementation for this stub server. code := fmt.Sprintf("%s:%s", challengeMethod, challenge) redirectQuery := redirectURL.Query() redirectQuery.Set("code", code) if state != "" { redirectQuery.Set("state", state) } redirectURL.RawQuery = redirectQuery.Encode() respBody := fmt.Sprintf(`Log In and Consent`, html.EscapeString(redirectURL.String())) resp.Header().Set("Content-Type", "text/html") resp.Header().Set("Content-Length", fmt.Sprintf("%d", len(respBody))) resp.Header().Set("X-Redirect-To", redirectURL.String()) // For robotic clients, using webbrowser.MockLauncher resp.WriteHeader(200) resp.Write([]byte(respBody)) } func (h handler) serveToken(resp http.ResponseWriter, req *http.Request) { if req.Method != "POST" { resp.WriteHeader(405) log.Printf("/token: unsupported request method %q", req.Method) return } if err := req.ParseForm(); err != nil { resp.WriteHeader(500) log.Printf("/token: error parsing body: %s", err) return } grantType := req.Form.Get("grant_type") log.Printf("/token: grant_type is %q", grantType) switch grantType { case "authorization_code": code := req.Form.Get("code") codeParts := strings.SplitN(code, ":", 2) if len(codeParts) != 2 { log.Printf("/token: invalid code %q", code) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(400) resp.Write([]byte(`{"error":"invalid_grant"}`)) return } codeVerifier := req.Form.Get("code_verifier") switch codeParts[0] { case "plain": if codeParts[1] != codeVerifier { log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], codeVerifier) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(400) resp.Write([]byte(`{"error":"invalid_grant"}`)) return } case "S256": h := sha256.New() h.Write([]byte(codeVerifier)) encVerifier := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) if codeParts[1] != encVerifier { log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], encVerifier) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(400) resp.Write([]byte(`{"error":"invalid_grant"}`)) return } default: log.Printf("/token: unsupported challenge method %q", codeParts[0]) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(400) resp.Write([]byte(`{"error":"invalid_grant"}`)) return } resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(200) resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`)) log.Println("/token: successful request") case "password": username := req.Form.Get("username") password := req.Form.Get("password") if username == "wrong" || password == "wrong" { // These special "credentials" allow testing for the error case. resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(400) resp.Write([]byte(`{"error":"invalid_grant"}`)) log.Println("/token: 'wrong' credentials") return } resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(200) resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`)) log.Println("/token: successful request") default: resp.WriteHeader(400) log.Printf("/token: unsupported grant type %q", grantType) } } func (h handler) serveRevoke(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(404) } func init() { Handler = handler{} }