package main import ( "encoding/base64" "encoding/json" "flag" "log" "math/big" "net/http" "os" "strings" "time" "github.com/golang-jwt/jwt/v4" ) // Implements /certs and /token func main() { var addr string var rsaPubPem string var rsaPem string var tlsCert string var tlsKey string var tokenExpires int var tokenScope string flag.StringVar(&addr, "a", "localhost:8080", "Address to serve on") flag.StringVar(&rsaPubPem, "rsaPubPem", "", "rsa pubkey in pem format (path)") flag.StringVar(&rsaPem, "rsaPem", "", "rsa privkey in pem format (path)") flag.StringVar(&tlsCert, "cert", "", "tls cert") flag.StringVar(&tlsKey, "key", "", "tls key") flag.IntVar(&tokenExpires, "expires", 60, "Expiration of the token in seconds (default: 360))") flag.StringVar(&tokenScope, "scope", "", "The scope of the token (default: not set)") flag.Parse() if rsaPubPem == "" || rsaPem == "" { panic("path to rsa keys needed") } mux := http.NewServeMux() mux.HandleFunc("/certs", func(w http.ResponseWriter, r *http.Request) { type key struct { Kid string `json:"kid"` Kty string `json:"kty"` Alg string `json:"alg"` N string `json:"n"` E string `json:"e"` } rsaPubBytes, err := os.ReadFile(rsaPubPem) if err != nil { panic(err) } pubKey, err := jwt.ParseRSAPublicKeyFromPEM(rsaPubBytes) if err != nil { panic(err) } k := key{ Kid: "key-id", Kty: "RSA", Alg: "RS256", N: strings.TrimRight(base64.URLEncoding.EncodeToString(pubKey.N.Bytes()), "="), E: strings.TrimRight(base64.URLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()), "="), } type response struct { Keys []key `json:"keys"` } err = json.NewEncoder(w).Encode(response{ Keys: []key{ k, }, }) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") }) mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { type customClaims struct { Type string `json:"typ"` ExpiresAt int64 `json:"exp"` IssuedAt int64 `json:"iat"` RHOrgID string `json:"rh-org-id"` jwt.Claims } if err := r.ParseForm(); err != nil { panic(err) } var cc customClaims switch r.Form.Get("grant_type") { case "refresh_token": cc = customClaims{ Type: "Bearer", ExpiresAt: 0, IssuedAt: time.Now().Unix(), // Use refresh_token as rh-org-id RHOrgID: r.Form.Get("refresh_token"), } case "client_credentials": cc = customClaims{ Type: "Bearer", ExpiresAt: 0, IssuedAt: time.Now().Unix(), // Use client_secret as rh-org-id RHOrgID: r.Form.Get("client_secret"), } default: w.WriteHeader(http.StatusBadRequest) return } token := jwt.NewWithClaims(jwt.SigningMethodRS256, cc) token.Header["kid"] = "key-id" rsaPrivBytes, err := os.ReadFile(rsaPem) if err != nil { panic(err) } privKey, err := jwt.ParseRSAPrivateKeyFromPEM(rsaPrivBytes) if err != nil { panic(err) } tokenStr, err := token.SignedString(privKey) if err != nil { panic(err) } // See https://datatracker.ietf.org/doc/html/rfc6749 type response struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` // required ExpiresIn int `json:"expires_in,omitempty"` // lifetime in seconds Scope string `json:"scope,omitempty"` } err = json.NewEncoder(w).Encode(response{ AccessToken: tokenStr, TokenType: "Bearer", ExpiresIn: tokenExpires, Scope: tokenScope, }) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") }) //nolint:gosec if tlsCert != "" && tlsKey != "" { log.Fatal(http.ListenAndServeTLS(addr, tlsCert, tlsKey, mux)) } else { log.Fatal(http.ListenAndServe(addr, mux)) } }