Files
upstream-ory-kratos/selfservice/strategy/oidc/provider_uaepass.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

183 lines
5.9 KiB
Go

// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/ory/herodot"
"github.com/ory/x/httpx"
"github.com/ory/x/otelx"
)
var _ OAuth2Provider = (*ProviderUAEPass)(nil)
// UAE PASS scopes for different user types
// See: https://docs.uaepass.ae/feature-guides/authentication/web-application/endpoints
//
// For UAE Citizens/Residents:
// - urn:uae:digitalid:profile:general - General profile information
// - urn:uae:digitalid:profile:general:profileType - Profile type (SOP1/SOP2/SOP3)
// - urn:uae:digitalid:profile:general:unifiedId - Unified ID
//
// For Visitors:
// - urn:uae:digitalid:profile - Basic profile
//
// Common scopes:
// - openid - Required for OIDC flows (returns id_token)
// ProviderUAEPass implements the OAuth2Provider interface for UAE PASS.
// UAE PASS is the UAE's official digital identity platform that allows
// citizens, residents, and visitors to authenticate with government and
// private sector services.
type ProviderUAEPass struct {
*ProviderGenericOIDC
baseURL string // Base URL for UAE PASS (e.g., https://id.uaepass.ae or https://stg-id.uaepass.ae)
userinfoURL string // Can be overridden in tests
}
// NewProviderUAEPass creates a new UAE PASS provider.
// The IssuerURL configuration determines which UAE PASS environment to use:
// - Production: https://id.uaepass.ae (default if not set)
// - Staging: https://stg-id.uaepass.ae
func NewProviderUAEPass(
config *Configuration,
reg Dependencies,
) Provider {
// Use IssuerURL if provided, otherwise default to production
baseURL := config.IssuerURL
if baseURL == "" {
baseURL = "https://id.uaepass.ae"
}
// Normalize: remove trailing slash if present
baseURL = strings.TrimSuffix(baseURL, "/")
// UAE PASS doesn't support PKCE discovery, so treat "auto" (or unset) as "force"
// since UAE PASS does support PKCE.
if config.PKCE == "" || config.PKCE == "auto" {
config.PKCE = "force"
}
return &ProviderUAEPass{
ProviderGenericOIDC: &ProviderGenericOIDC{
config: config,
reg: reg,
},
baseURL: baseURL,
userinfoURL: baseURL + "/idshub/userinfo",
}
}
// oauth2 returns the OAuth2 configuration for UAE PASS.
func (p *ProviderUAEPass) oauth2(ctx context.Context) *oauth2.Config {
return &oauth2.Config{
ClientID: p.config.ClientID,
ClientSecret: p.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: p.baseURL + "/idshub/authorize",
TokenURL: p.baseURL + "/idshub/token",
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: p.config.Scope,
RedirectURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)),
}
}
// OAuth2 returns the OAuth2 configuration for UAE PASS.
func (p *ProviderUAEPass) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return p.oauth2(ctx), nil
}
// UAE PASS acr_values for different authentication levels
// UAEPassACRWeb is the default authentication level for web-based authentication
const UAEPassACRWeb = "urn:safelayer:tws:policies:authentication:level:low" //nolint:gosec // not a credential, this is an ACR URN
// AuthCodeURLOptions returns OAuth2 authorization URL options.
// UAE PASS requires acr_values parameter for authentication.
// The default is web-based authentication. To use mobile app-to-app
// authentication, pass acr_values via upstream_parameters.
func (p *ProviderUAEPass) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("acr_values", UAEPassACRWeb),
}
}
// Claims fetches user claims from the UAE PASS userinfo endpoint.
// UAE PASS requires the access token to be passed as a Bearer token in the Authorization header.
func (p *ProviderUAEPass) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) {
ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderUAEPass.Claims")
defer otelx.End(span, &err)
ctx, client := httpx.SetOAuth2(ctx, p.reg.HTTPClient(ctx), p.oauth2(ctx), exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, p.userinfoURL, nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError().
WithReason("failed to create HTTP request").
WithDetail("url", p.userinfoURL).
WithError(err.Error()))
}
// UAE PASS expects the access token as a Bearer token in the Authorization header
req.Header.Set("Authorization", "Bearer "+exchange.AccessToken)
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().
WithReason("failed to make HTTP request to UAE PASS userinfo endpoint").
WithDetail("url", p.userinfoURL).
WithError(err.Error()))
}
defer func() { _ = resp.Body.Close() }()
body := io.LimitReader(resp.Body, 64*1024) // 64 KiB limit
if resp.StatusCode != http.StatusOK {
rawResponse, _ := io.ReadAll(body)
return nil, errors.WithStack(herodot.ErrUpstreamError().
WithReason("UAE PASS userinfo endpoint returned non-200 status").
WithDetail("url", p.userinfoURL).
WithDetail("external_error", string(rawResponse)).
WithDetail("external_status_code", resp.StatusCode))
}
var rawClaims map[string]interface{}
if err := json.NewDecoder(body).Decode(&rawClaims); err != nil {
return nil, errors.WithStack(herodot.ErrUpstreamError().
WithReason("failed to decode UAE PASS userinfo response").
WithDetail("url", p.userinfoURL).
WithError(err.Error()))
}
str := func(key string) string {
if v, ok := rawClaims[key].(string); ok {
return v
}
return ""
}
// Map UAE PASS profile to standard OIDC claims
claims := &Claims{
Subject: str("sub"),
Issuer: p.baseURL,
Email: str("email"),
Name: str("fullnameEN"),
GivenName: str("firstnameEN"),
LastName: str("lastnameEN"),
RawClaims: rawClaims,
}
return claims, nil
}