Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion internal/clientcredentials/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"slices"
"strconv"
"strings"
"sync"
)
Expand Down Expand Up @@ -38,6 +39,15 @@ func handleBasicAuth(authHeader string) (clientID, clientSecret string, ok bool)
return creds[0], creds[1], true
}

// Generate a fake access token for valid client credentials.
// Supports both Basic Auth and form-encoded client_id/client_secret
// Valid Credentials:
// - client_id: "speakeasy-sdks"
// - client_secret: "supersecret-<anything>"
//
// Optional Query Parameters:
// - token_type: defaults to "Bearer"
// - expires_in: defaults to 0 (already expired)
func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
var clientID, clientSecret string
err := r.ParseForm()
Expand Down Expand Up @@ -89,6 +99,16 @@ func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
tokenType = "Bearer" // default
}

expiresIn := 0 // default
expiresInStr := r.URL.Query().Get("expires_in")
if expiresInStr != "" {
var err error
if expiresIn, err = strconv.Atoi(expiresInStr); err != nil {
http.Error(w, "could not convert expires_in query parameter to integer", http.StatusBadRequest)
return
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

accessToken := firstAccessToken

_, ok := state.Load(clientSecret)
Expand All @@ -109,7 +129,7 @@ func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
response := tokenResponse{
AccessToken: accessToken,
TokenType: tokenType,
ExpiresIn: 0,
ExpiresIn: expiresIn,
}

if err := json.NewEncoder(w).Encode(response); err != nil {
Expand Down
76 changes: 62 additions & 14 deletions internal/clientcredentials/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (

func TestHandleTokenRequest(t *testing.T) {
tests := []struct {
name string
setupRequest func() *http.Request
wantStatus int
name string
setupRequest func() *http.Request
wantStatus int
wantAccessToken string
}{
{
Expand All @@ -26,11 +26,11 @@ func TestHandleTokenRequest(t *testing.T) {
form.Set("client_secret", "supersecret-123")
form.Set("scope", "read write")

req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req
},
wantStatus: http.StatusOK,
wantStatus: http.StatusOK,
wantAccessToken: firstAccessToken,
},
{
Expand All @@ -40,7 +40,7 @@ func TestHandleTokenRequest(t *testing.T) {
form.Set("grant_type", "client_credentials")
form.Set("scope", "read write")

req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

// Create basic auth header
Expand All @@ -49,13 +49,13 @@ func TestHandleTokenRequest(t *testing.T) {

return req
},
wantStatus: http.StatusOK,
wantStatus: http.StatusOK,
wantAccessToken: firstAccessToken,
},
{
name: "invalid basic auth format",
setupRequest: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/token", nil)
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", nil)
req.Header.Set("Authorization", "Basic invalid-base64")
return req
},
Expand All @@ -64,7 +64,7 @@ func TestHandleTokenRequest(t *testing.T) {
{
name: "missing credentials in basic auth",
setupRequest: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "/token", nil)
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", nil)
// Encode just username without password
creds := base64.StdEncoding.EncodeToString([]byte("speakeasy-sdks"))
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", creds))
Expand All @@ -77,7 +77,7 @@ func TestHandleTokenRequest(t *testing.T) {
setupRequest: func() *http.Request {
form := url.Values{}
form.Set("grant_type", "client_credentials")
req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", strings.NewReader(form.Encode()))
Comment thread
cursor[bot] marked this conversation as resolved.
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
creds := base64.StdEncoding.EncodeToString([]byte("wrong:wrong"))
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", creds))
Expand All @@ -93,7 +93,7 @@ func TestHandleTokenRequest(t *testing.T) {
form.Set("grant_type", "client_credentials")
form.Set("scope", "unknown") // missing write scope

req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

creds := base64.StdEncoding.EncodeToString([]byte("speakeasy-sdks:supersecret-123"))
Expand All @@ -110,15 +110,49 @@ func TestHandleTokenRequest(t *testing.T) {
form.Set("grant_type", "client_credentials")
form.Set("scope", "read write")

req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

creds := base64.StdEncoding.EncodeToString([]byte("speakeasy-sdks:supersecret-123"))
req.Header.Set("Authorization", fmt.Sprintf("BASIC %s", creds))

return req
},
wantStatus: http.StatusOK,
wantStatus: http.StatusOK,
wantAccessToken: firstAccessToken,
},
{
name: "Non-expired token",
setupRequest: func() *http.Request {
form := url.Values{}
form.Set("grant_type", "client_credentials")
form.Set("client_id", "speakeasy-sdks")
form.Set("client_secret", "supersecret-123")
form.Set("scope", "read write")

req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token?expires_in=60", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

return req
},
wantStatus: http.StatusOK,
wantAccessToken: firstAccessToken,
},
{
name: "custom token type",
setupRequest: func() *http.Request {
form := url.Values{}
form.Set("grant_type", "client_credentials")
form.Set("client_id", "speakeasy-sdks")
form.Set("client_secret", "supersecret-123")
form.Set("scope", "read write")

req := httptest.NewRequest(http.MethodPost, "/clientcredentials/token?token_type=custom", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

return req
},
wantStatus: http.StatusOK,
wantAccessToken: firstAccessToken,
},
}
Expand All @@ -136,7 +170,21 @@ func TestHandleTokenRequest(t *testing.T) {
if !strings.Contains(w.Body.String(), tt.wantAccessToken) {
t.Errorf("HandleTokenRequest() response doesn't contain expected access token")
}

// Check expires_in query parameter
if tt.name == "Non-expired token" {
if !strings.Contains(w.Body.String(), `"expires_in":60`) {
t.Errorf("HandleTokenRequest() response doesn't contain expected expires_in value")
}
}

// Check token_type query parameter
if tt.name == "custom token type" {
if !strings.Contains(w.Body.String(), `"token_type":"custom"`) {
t.Errorf("HandleTokenRequest() response doesn't contain expected token_type value")
}
}
}
})
}
}
}
Loading