package opaal import ( "bytes" "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" "io" "net/http" "net/url" "slices" "strings" "time" "github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/util" "github.com/lestrrat-go/jwx/v2/jwk" ) func (client *Client) AddTrustedIssuer(url string, issuer string, key jwk.Key, subject string, expires time.Duration) ([]byte, error) { // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers quotedScopes := make([]string, len(client.Scope)) for i, s := range client.Scope { quotedScopes[i] = fmt.Sprintf("\"%s\"", s) } jwkstr, err := json.Marshal(key) if err != nil { return nil, fmt.Errorf("failed to marshal JWK: %v", err) } // NOTE: Can also include "jwks_uri" instead data := []byte(fmt.Sprintf("{"+ "\"allow_any_subject\": false,"+ "\"issuer\": \"%s\","+ "\"subject\": \"%s\","+ "\"expires_at\": \"%v\","+ "\"jwk\": %v,"+ "\"scope\": [ %s ]"+ "}", issuer, subject, time.Now().Add(expires).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) if err != nil { return nil, fmt.Errorf("failed to make request: %v", err) } req.Header.Add("Content-Type", "application/json") // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) res, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to do request: %v", err) } defer res.Body.Close() return io.ReadAll(res.Body) } func (client *Client) AddTrustedIssuerWithIdentityProvider(url string, idp *oidc.IdentityProvider, subject string, expires time.Duration) ([]byte, error) { // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers key, ok := idp.Jwks.Key(0) if !ok { return nil, fmt.Errorf("no keys found in key set") } return client.AddTrustedIssuer(url, idp.Issuer, key, subject, expires) } func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) { _, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) if err != nil { return false, fmt.Errorf("failed to make request: %v", err) } // TODO: need to check contents of actual response return true, nil } func (client *Client) GetOAuthClient(clientUrl string) error { _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) if err != nil { return fmt.Errorf("failed to make request: %v", err) } fmt.Printf("GetOAuthClient: %v\n", string(b)) var data []map[string]any err = json.Unmarshal(b, &data) if err != nil { return fmt.Errorf("failed to unmarshal JSON: %v", err) } index := slices.IndexFunc(data, func(c map[string]any) bool { if c["client_id"] == nil { return false } return c["client_id"].(string) == client.Id }) if index < 0 { return fmt.Errorf("client not found") } // cast the redirect_uris from []any to []string and extract registration token foundClient := data[index] for _, uri := range foundClient["redirect_uris"].([]any) { client.RedirectUris = append(client.RedirectUris, uri.(string)) } if foundClient["registration-access-token"] != nil { client.RegistrationAccessToken = foundClient["registration-access-token"].(string) } return nil } func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) { // hydra endpoint: POST /clients audience = util.QuoteArrayStrings(audience) body := httpx.Body(fmt.Sprintf(`{ "client_id": "%s", "client_name": "%s", "client_secret": "%s", "token_endpoint_auth_method": "client_secret_post", "scope": "%s", "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], "response_types": ["token"], "redirect_uris": ["http://127.0.0.1:3333/callback"], "state": 12345678910, "audience": [%s] }`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","), )) headers := httpx.Headers{ "Content-Type": "application/json", } _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, []byte(body), headers) if err != nil { return nil, fmt.Errorf("failed to make request: %v", err) } var rjson map[string]any err = json.Unmarshal(b, &rjson) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %v", err) } // check for error first errJson := rjson["error"] if errJson == nil { // set the client ID and secret of registered client client.Id = rjson["client_id"].(string) client.Secret = rjson["client_secret"].(string) client.RegistrationAccessToken = rjson["registration_access_token"].(string) } else { return b, nil } return b, err } func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { // hydra endpoint: POST /oauth2/register audience = util.QuoteArrayStrings(audience) body := httpx.Body(fmt.Sprintf(`{ "client_name": "opaal", "token_endpoint_auth_method": "client_secret_post", "scope": "%s", "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], "response_types": ["token"], "redirect_uris": ["http://127.0.0.1:3333/callback"], "state": 12345678910, "audience": [%s] }`, strings.Join(client.Scope, " "), strings.Join(audience, ","), )) headers := httpx.Headers{ "Content-Type": "application/json", } _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, body, headers) if err != nil { return nil, fmt.Errorf("failed to make request: %v", err) } var rjson map[string]any err = json.Unmarshal(b, &rjson) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %v", err) } // check for error first errJson := rjson["error"] if errJson == nil { // set the client ID and secret of registered client client.Id = rjson["client_id"].(string) client.Secret = rjson["client_secret"].(string) client.RegistrationAccessToken = rjson["registration_access_token"].(string) } else { return b, nil } return b, err } func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) { // set the authorization header body := []byte("grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&scope=" + strings.Join(client.Scope, "+") + "&client_id=" + client.Id + "&client_secret=" + client.Secret + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // FIXME: needs to not be hardcorded "&response_type=token" + "&state=12345678910", ) headers := httpx.Headers{ "Authorization": "Bearer " + client.RegistrationAccessToken, "Content-Type": "application/x-www-form-urlencoded", } _, b, err := httpx.MakeHttpRequest(authorizeUrl, http.MethodPost, body, headers) if err != nil { return nil, fmt.Errorf("failed to make HTTP request: %v", err) } return b, nil } func (client *Client) PerformTokenGrant(clientUrl string, encodedJwt string) ([]byte, error) { // hydra endpoint: /oauth/token body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&client_id=" + client.Id + "&client_secret=" + client.Secret + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") // add optional params if valid if encodedJwt != "" { body += "&assertion=" + encodedJwt } if client.Scope != nil || len(client.Scope) > 0 { body += "&scope=" + strings.Join(client.Scope, "+") } headers := httpx.Headers{ "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Bearer " + client.RegistrationAccessToken, } _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers) // set flow ID back to empty string to indicate a completed flow client.FlowId = "" return b, err } func (client *Client) DeleteOAuthClient(clientUrl string) error { _, _, err := httpx.MakeHttpRequest(clientUrl+"/"+client.Id, http.MethodDelete, nil, nil) if err != nil { return fmt.Errorf("failed to make request: %v", err) } return nil }