Serve the OAuth client metadata document
A public client identifies itself to the provider with a hosted Client ID Metadata Document. Serve it unauthenticated and outside the auth group, since the provider fetches it server-to-server, exposing the deployment-derived client_id and redirect_uri alongside the Probo brand name, homepage and logo. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
71
pkg/server/api/console/v1/connector_oauth_client_metadata.go
Normal file
71
pkg/server/api/console/v1/connector_oauth_client_metadata.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
|
"go.probo.inc/probo/pkg/connector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// oauthClientMetadata is the OAuth Client ID Metadata Document (CIMD)
|
||||||
|
// published for public-client connectors. The deployment's
|
||||||
|
// (baseURL + CIMDMetadataPath) URL is the OAuth client_id; providers such as
|
||||||
|
// PostHog fetch this document server-to-server during authorization to learn
|
||||||
|
// the client's identity and allowed redirect URIs, so no app pre-registration
|
||||||
|
// is needed. Public clients authenticate with PKCE (token_endpoint_auth_method
|
||||||
|
// "none") rather than a client secret.
|
||||||
|
// Probo brand fields shown to the end user on the provider's consent screen.
|
||||||
|
// These describe the Probo product itself (not the per-tenant deployment), so
|
||||||
|
// they are the canonical brand homepage and logo rather than the baseURL.
|
||||||
|
const (
|
||||||
|
proboBrandURI = "https://www.getprobo.com"
|
||||||
|
proboLogoURI = "https://www.getprobo.com/probo-logo-only.svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthClientMetadata struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientName string `json:"client_name"`
|
||||||
|
ClientURI string `json:"client_uri"`
|
||||||
|
LogoURI string `json:"logo_uri"`
|
||||||
|
RedirectURIs []string `json:"redirect_uris"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||||
|
GrantTypes []string `json:"grant_types"`
|
||||||
|
ResponseTypes []string `json:"response_types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnectorOAuthClientMetadata serves the public, unauthenticated CIMD
|
||||||
|
// document. It is intentionally outside the auth middleware group: the OAuth
|
||||||
|
// provider fetches it without any Probo credentials.
|
||||||
|
func handleConnectorOAuthClientMetadata(baseURL *baseurl.BaseURL) http.HandlerFunc {
|
||||||
|
doc := oauthClientMetadata{
|
||||||
|
ClientID: baseURL.WithPath(connector.CIMDMetadataPath).MustString(),
|
||||||
|
ClientName: "Probo",
|
||||||
|
ClientURI: proboBrandURI,
|
||||||
|
LogoURI: proboLogoURI,
|
||||||
|
RedirectURIs: []string{baseURL.WithPath(connector.CallbackPath).MustString()},
|
||||||
|
TokenEndpointAuthMethod: "none",
|
||||||
|
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||||
|
ResponseTypes: []string{"code"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=300")
|
||||||
|
_ = json.NewEncoder(w).Encode(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHandleConnectorOAuthClientMetadata verifies the public CIMD document:
|
||||||
|
// PostHog fetches it server-to-server during authorization, so client_id,
|
||||||
|
// redirect_uris (derived from the deployment base URL) and the public-client
|
||||||
|
// token_endpoint_auth_method must be exactly right or the OAuth flow breaks.
|
||||||
|
func TestHandleConnectorOAuthClientMetadata(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
base, err := baseurl.Parse("https://probo.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
handleConnectorOAuthClientMetadata(base)(
|
||||||
|
rec,
|
||||||
|
httptest.NewRequest(http.MethodGet, "/api/console/v1/connectors/oauth-client-metadata", nil),
|
||||||
|
)
|
||||||
|
|
||||||
|
res := rec.Result()
|
||||||
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||||
|
assert.Equal(t, "application/json", res.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
var doc struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientName string `json:"client_name"`
|
||||||
|
ClientURI string `json:"client_uri"`
|
||||||
|
LogoURI string `json:"logo_uri"`
|
||||||
|
RedirectURIs []string `json:"redirect_uris"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||||
|
GrantTypes []string `json:"grant_types"`
|
||||||
|
ResponseTypes []string `json:"response_types"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&doc))
|
||||||
|
|
||||||
|
// Functional fields are deployment-derived (must match where the OAuth
|
||||||
|
// flow actually runs)...
|
||||||
|
assert.Equal(t, "https://probo.example.com/api/console/v1/connectors/oauth-client-metadata", doc.ClientID)
|
||||||
|
assert.Equal(t, []string{"https://probo.example.com/api/console/v1/connectors/complete"}, doc.RedirectURIs)
|
||||||
|
// ...while the brand fields shown on the consent screen are the canonical
|
||||||
|
// Probo product identity, NOT the per-tenant deployment URL.
|
||||||
|
assert.Equal(t, "Probo", doc.ClientName)
|
||||||
|
assert.Equal(t, "https://www.getprobo.com", doc.ClientURI)
|
||||||
|
assert.Equal(t, "https://www.getprobo.com/probo-logo-only.svg", doc.LogoURI)
|
||||||
|
assert.Equal(t, "none", doc.TokenEndpointAuthMethod, "public client must advertise token_endpoint_auth_method none")
|
||||||
|
assert.Contains(t, doc.GrantTypes, "authorization_code")
|
||||||
|
assert.Contains(t, doc.GrantTypes, "refresh_token")
|
||||||
|
assert.Equal(t, []string{"code"}, doc.ResponseTypes)
|
||||||
|
}
|
||||||
@@ -128,6 +128,12 @@ func NewMux(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Public, unauthenticated: the OAuth Client ID Metadata Document (CIMD)
|
||||||
|
// is fetched server-to-server by public-client providers (PostHog)
|
||||||
|
// during authorization, with no Probo credentials. Mounted outside the
|
||||||
|
// auth group above.
|
||||||
|
r.Get("/connectors/oauth-client-metadata", handleConnectorOAuthClientMetadata(baseURL))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user