diff --git a/go.mod b/go.mod
index 94cd288c648e0..24c2a87588485 100644
--- a/go.mod
+++ b/go.mod
@@ -167,6 +167,7 @@ require (
github.com/okta/okta-sdk-golang/v2 v2.20.0
github.com/opencontainers/go-digest v1.0.0
github.com/opensearch-project/opensearch-go/v2 v2.3.0
+ github.com/oracle/oci-go-sdk/v65 v65.81.0
github.com/parquet-go/parquet-go v0.24.0
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
@@ -505,6 +506,7 @@ require (
github.com/sigstore/rekor v1.3.6 // indirect
github.com/sigstore/timestamp-authority v1.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sony/gobreaker v0.5.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
diff --git a/go.sum b/go.sum
index de4ade8412e83..b3745494ac076 100644
--- a/go.sum
+++ b/go.sum
@@ -1353,6 +1353,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -1961,6 +1962,8 @@ github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsr
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/oracle/oci-go-sdk/v65 v65.81.0 h1:uyAdy7N7q3cj090zrLCCL+IbL3JHd4IXZi2N5epXeAk=
+github.com/oracle/oci-go-sdk/v65 v65.81.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/parquet-go/parquet-go v0.24.0 h1:VrsifmLPDnas8zpoHmYiWDZ1YHzLmc7NmNwPGkI2JM4=
github.com/parquet-go/parquet-go v0.24.0/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
@@ -2158,6 +2161,8 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/snowflakedb/gosnowflake v1.13.0 h1:NQoy4mnHUmBuruJhzAGVRO9YLpFxayYTCLf+dxvG7bk=
github.com/snowflakedb/gosnowflake v1.13.0/go.mod h1:nwiPNHaS3EGxnW1rr10ascVYFLA4EKrqMX2TxPt0+N4=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum
index fc152a6203026..3f0f35e46bbe0 100644
--- a/integrations/event-handler/go.sum
+++ b/integrations/event-handler/go.sum
@@ -668,6 +668,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/oracle/oci-go-sdk/v65 v65.81.0 h1:uyAdy7N7q3cj090zrLCCL+IbL3JHd4IXZi2N5epXeAk=
+github.com/oracle/oci-go-sdk/v65 v65.81.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible h1:IWzUvJ72xMjmrjR9q3H1PF+jwdN0uNQiR2t1BLNalyo=
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=
@@ -766,6 +768,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod
index 4e3cda04f5ade..29c833f9f934c 100644
--- a/integrations/terraform/go.mod
+++ b/integrations/terraform/go.mod
@@ -283,6 +283,7 @@ require (
github.com/onsi/ginkgo/v2 v2.21.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/oracle/oci-go-sdk/v65 v65.81.0 // indirect
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible // indirect
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
@@ -313,6 +314,7 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sijms/go-ora/v2 v2.8.23 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sony/gobreaker v0.5.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum
index f13d27b478b92..a22557038bb65 100644
--- a/integrations/terraform/go.sum
+++ b/integrations/terraform/go.sum
@@ -549,6 +549,7 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
+github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
@@ -1016,6 +1017,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ=
github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8=
+github.com/oracle/oci-go-sdk/v65 v65.81.0 h1:uyAdy7N7q3cj090zrLCCL+IbL3JHd4IXZi2N5epXeAk=
+github.com/oracle/oci-go-sdk/v65 v65.81.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
github.com/parquet-go/parquet-go v0.24.0 h1:VrsifmLPDnas8zpoHmYiWDZ1YHzLmc7NmNwPGkI2JM4=
github.com/parquet-go/parquet-go v0.24.0/go.mod h1:OqBBRGBl7+llplCvDMql8dEKaDqjaFA/VAPw+OJiNiw=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
@@ -1141,6 +1144,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -1153,6 +1158,7 @@ github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GB
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -1164,6 +1170,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
diff --git a/lib/auth/join/join.go b/lib/auth/join/join.go
index 940cd3b211720..7d1648becc4d0 100644
--- a/lib/auth/join/join.go
+++ b/lib/auth/join/join.go
@@ -21,6 +21,7 @@ import (
"crypto"
"crypto/x509"
"log/slog"
+ "net/http"
"os"
"time"
@@ -40,6 +41,7 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/join/iam"
+ "github.com/gravitational/teleport/lib/auth/join/oracle"
"github.com/gravitational/teleport/lib/auth/state"
"github.com/gravitational/teleport/lib/bitbucket"
"github.com/gravitational/teleport/lib/circleci"
@@ -811,10 +813,31 @@ func registerUsingTPMMethod(
return certs, trace.Wrap(err)
}
+func mapFromHeader(header http.Header) map[string]string {
+ out := make(map[string]string, len(header))
+ for k := range header {
+ out[k] = header.Get(k)
+ }
+ return out
+}
+
func registerUsingOracleMethod(
ctx context.Context, client joinServiceClient, token string, hostKeys *newHostKeys, params RegisterParams,
) (*proto.Certs, error) {
- return nil, trace.NotImplemented("Not implemented")
+ certs, err := client.RegisterUsingOracleMethod(
+ ctx,
+ registerUsingTokenRequestForParams(token, hostKeys, params),
+ func(challenge string) (*proto.OracleSignedRequest, error) {
+ innerHeaders, outerHeaders, err := oracle.CreateSignedRequest(challenge)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &proto.OracleSignedRequest{
+ Headers: mapFromHeader(outerHeaders),
+ PayloadHeaders: mapFromHeader(innerHeaders),
+ }, nil
+ })
+ return certs, trace.Wrap(err)
}
// readCA will read in CA that will be used to validate the certificate that
diff --git a/lib/auth/join/oracle/oracle.go b/lib/auth/join/oracle/oracle.go
new file mode 100644
index 0000000000000..0a037d6b108f1
--- /dev/null
+++ b/lib/auth/join/oracle/oracle.go
@@ -0,0 +1,322 @@
+// Teleport
+// Copyright (C) 2025 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package oracle
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/gravitational/trace"
+ "github.com/oracle/oci-go-sdk/v65/common"
+ "github.com/oracle/oci-go-sdk/v65/common/auth"
+
+ "github.com/gravitational/teleport/api"
+ "github.com/gravitational/teleport/lib/defaults"
+)
+
+const teleportUserAgent = "teleport/" + api.Version
+
+const (
+ // DateHeader is the header containing the date to send to Oracle.
+ DateHeader = "x-date"
+ // ChallengeHeader is the header containing the Teleport-generated challenge
+ // string to send to Oracle.
+ ChallengeHeader = "x-teleport-challenge"
+)
+
+const (
+ tenancyClaim = "opc-tenant"
+ compartmentClaim = "opc-compartment"
+ instanceClaim = "opc-instance"
+)
+
+type authenticateClientDetails struct {
+ RequestHeaders http.Header `json:"requestHeaders"`
+}
+
+type authenticateClientRequest struct {
+ Date string `contributesTo:"header" name:"x-date"`
+ Challenge string `contributesTo:"header" name:"x-teleport-challenge"`
+ UserAgent string `contributesTo:"header" name:"User-Agent"`
+ Details authenticateClientDetails `contributesTo:"body"`
+}
+
+// Claims are the claims returned by the authenticateClient endpoint.
+type Claims struct {
+ // TenancyID is the ID of the instance's tenant.
+ TenancyID string `json:"tenant_id"`
+ // CompartmentID is the ID of the instance's compartment.
+ CompartmentID string `json:"compartment_id"`
+ // InstanceID is the instance's ID.
+ InstanceID string `json:"-"`
+}
+
+// Region extracts the region from an instance's claims.
+func (c Claims) Region() string {
+ region, err := ParseRegionFromOCID(c.InstanceID)
+ if err != nil {
+ return ""
+ }
+ return region
+}
+
+type claim struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+}
+
+type principal struct {
+ Claims []claim `json:"claims"`
+}
+
+func (p principal) getClaims() Claims {
+ var claims Claims
+ for _, claim := range p.Claims {
+ switch claim.Key {
+ case tenancyClaim:
+ claims.TenancyID = claim.Value
+ case compartmentClaim:
+ claims.CompartmentID = claim.Value
+ case instanceClaim:
+ claims.InstanceID = claim.Value
+ }
+ }
+ return claims
+}
+
+type authenticateClientResult struct {
+ ErrorMessage string `json:"errorMessage,omitempty"`
+ Principal principal `json:"principal,omitempty"`
+}
+
+type authenticateClientResponse struct {
+ Result authenticateClientResult `presentIn:"body"`
+}
+
+func newAuthenticateClientRequest(time time.Time, challenge string, headers http.Header) authenticateClientRequest {
+ req := authenticateClientRequest{
+ Date: time.UTC().Format(http.TimeFormat),
+ Challenge: challenge,
+ UserAgent: teleportUserAgent,
+ Details: authenticateClientDetails{
+ RequestHeaders: headers,
+ },
+ }
+ // Avoid a null request body.
+ if len(headers) == 0 {
+ req.Details.RequestHeaders = http.Header{}
+ }
+ return req
+}
+
+func createAuthHTTPRequest(region string, auth authenticateClientRequest) (*http.Request, error) {
+ req, err := common.MakeDefaultHTTPRequestWithTaggedStruct(
+ http.MethodPost,
+ "",
+ auth,
+ )
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ endpointURL, err := url.Parse(fmt.Sprintf("https://auth.%s.oraclecloud.com/v1/authentication/authenticateClient", region))
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ // Manually set the host header so it will be sent as part of the grpc.
+ req.Header.Set("Host", endpointURL.Host)
+ req.Host = endpointURL.Host
+ req.URL = endpointURL
+
+ // If no headers were provided, this is the inner header payload and we need
+ // to explicitly include (request-target).
+ if len(auth.Details.RequestHeaders) == 0 {
+ req.Header.Set("(request-target)", strings.ToLower(http.MethodPost)+" "+endpointURL.RequestURI())
+ }
+ return &req, nil
+}
+
+// CreateSignedRequest creates a signed HTTP request to
+// https://auth..oraclecloud.com/v1/authentication/authenticateClient.
+// The returned headers should be sent to an auth server as part of
+// RegisterUsingOracleMethod.
+func CreateSignedRequest(challenge string) (innerHeaders, outerHeaders http.Header, err error) {
+ provider, err := auth.InstancePrincipalConfigurationProvider()
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ inner, outer, err := createSignedRequest(provider, challenge)
+ return inner, outer, trace.Wrap(err)
+}
+
+func createSignedRequest(provider common.ConfigurationProvider, challenge string) (innerHeaders, outerHeaders http.Header, err error) {
+ signedHeaders := append(common.DefaultGenericHeaders(), DateHeader, ChallengeHeader)
+ signer := common.RequestSigner(provider, signedHeaders, common.DefaultBodyHeaders())
+ region, err := provider.Region()
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ now := time.Now().UTC()
+ innerReq, err := createAuthHTTPRequest(region, newAuthenticateClientRequest(now, challenge, nil))
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ signer.Sign(innerReq)
+
+ outerReq, err := createAuthHTTPRequest(region, newAuthenticateClientRequest(now, challenge, innerReq.Header))
+ if err != nil {
+ return nil, nil, trace.Wrap(err)
+ }
+ signer.Sign(outerReq)
+ return innerReq.Header, outerReq.Header, nil
+}
+
+// GetAuthorizationHeaderValues gets the key-value pairs encoded in the
+// Authorization header as described in the [Oracle API docs].
+//
+// [Oracle API docs]: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm#five
+func GetAuthorizationHeaderValues(header http.Header) map[string]string {
+ rawValues, isSignature := strings.CutPrefix(header.Get("Authorization"), "Signature ")
+ if !isSignature {
+ return nil
+ }
+ keyValuePairs := strings.Split(rawValues, ",")
+ values := make(map[string]string, len(keyValuePairs))
+ for _, pair := range keyValuePairs {
+ k, v, isPair := strings.Cut(pair, "=")
+ if !isPair {
+ continue
+ }
+ values[k] = strings.Trim(v, "\"")
+ }
+ return values
+}
+
+// CreateRequestFromHeaders recreates an HTTP request to the authenticateClient
+// endpoint from its inner and outer headers.
+func CreateRequestFromHeaders(region string, innerHeaders, outerHeaders http.Header) (*http.Request, error) {
+ req, err := createAuthHTTPRequest(region, authenticateClientRequest{
+ Date: outerHeaders.Get(DateHeader),
+ Challenge: outerHeaders.Get(ChallengeHeader),
+ UserAgent: teleportUserAgent,
+ Details: authenticateClientDetails{
+ RequestHeaders: innerHeaders,
+ },
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ req.Header = outerHeaders
+ return req, nil
+}
+
+// FetchOraclePrincipalClaims executes a request to authenticateClient and parses
+// the response.
+func FetchOraclePrincipalClaims(ctx context.Context, req *http.Request) (Claims, error) {
+ client, err := defaults.HTTPClient()
+ if err != nil {
+ return Claims{}, trace.Wrap(err)
+ }
+ // Block redirects.
+ client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ }
+
+ authResp, err := client.Do(req.WithContext(ctx))
+ if err != nil {
+ return Claims{}, trace.Wrap(err)
+ }
+ defer authResp.Body.Close()
+ var resp authenticateClientResponse
+ unmarshalErr := common.UnmarshalResponse(authResp, &resp)
+ if authResp.StatusCode >= 300 || resp.Result.ErrorMessage != "" {
+ msg := resp.Result.ErrorMessage
+ if msg == "" {
+ msg = authResp.Status
+ }
+ return Claims{}, trace.AccessDenied("%v", msg)
+ }
+ if unmarshalErr != nil {
+ return Claims{}, trace.Wrap(unmarshalErr)
+ }
+ return resp.Result.Principal.getClaims(), nil
+}
+
+// Hack: StringToRegion will lazily load regions from a config file if its
+// input isn't in its hard-coded list, in a non-threadsafe way. Call it here
+// to load the config ahead of time so future calls are threadsafe.
+var _ = common.StringToRegion("")
+
+// ParseRegion parses a string into a full (not abbreviated) Oracle Cloud
+// region. It returns the empty string if the input is not a valid region.
+func ParseRegion(rawRegion string) (region, realm string) {
+ canonicalRegion := common.StringToRegion(rawRegion)
+ realm, err := canonicalRegion.RealmID()
+ if err != nil {
+ return "", ""
+ }
+ return string(canonicalRegion), realm
+}
+
+var ociRealms = map[string]struct{}{
+ "oc1": {}, "oc2": {}, "oc3": {}, "oc4": {}, "oc8": {}, "oc9": {},
+ "oc10": {}, "oc14": {}, "oc15": {}, "oc19": {}, "oc20": {}, "oc21": {},
+ "oc23": {}, "oc24": {}, "oc26": {}, "oc29": {}, "oc35": {},
+}
+
+// ParseRegionFromOCID parses an Oracle OCID and returns the embedded region.
+// It returns an error if the input is not a valid OCID.
+func ParseRegionFromOCID(ocid string) (string, error) {
+ // OCID format: ocid1...[REGION][.FUTURE USE].
+ // Check format.
+ ocidParts := strings.Split(ocid, ".")
+ switch len(ocidParts) {
+ case 5, 6:
+ default:
+ return "", trace.BadParameter("not an ocid")
+ }
+ // Check version.
+ if ocidParts[0] != "ocid1" {
+ return "", trace.BadParameter("invalid ocid version: %v", ocidParts[0])
+ }
+ // Check realm.
+ if _, ok := ociRealms[ocidParts[2]]; !ok {
+ return "", trace.BadParameter("invalid realm: %v", ocidParts[2])
+ }
+ resourceType := ocidParts[1]
+ region, realm := ParseRegion(ocidParts[3])
+ // Check type. Only instance OCIDs should have a region.
+ switch resourceType {
+ case "instance":
+ if region == "" {
+ return "", trace.BadParameter("invalid region: %v", region)
+ }
+ if realm != ocidParts[2] {
+ return "", trace.BadParameter("invalid realm %q for region %q", ocidParts[2], region)
+ }
+ case "compartment", "tenancy":
+ if ocidParts[3] != "" {
+ return "", trace.BadParameter("resource type %v should not have a region", resourceType)
+ }
+ default:
+ return "", trace.BadParameter("unsupported resource type: %v", resourceType)
+ }
+ return region, nil
+}
diff --git a/lib/auth/join/oracle/oracle_test.go b/lib/auth/join/oracle/oracle_test.go
new file mode 100644
index 0000000000000..03cdf64015f80
--- /dev/null
+++ b/lib/auth/join/oracle/oracle_test.go
@@ -0,0 +1,324 @@
+// Teleport
+// Copyright (C) 2025 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package oracle
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/oracle/oci-go-sdk/v65/common"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/lib/fixtures"
+)
+
+func TestGetAuthorizationHeaderValues(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ authHeader string
+ expectedValues map[string]string
+ }{
+ {
+ name: "ok",
+ authHeader: "Signature foo=bar,baz=\"quux\"",
+ expectedValues: map[string]string{
+ "foo": "bar",
+ "baz": "quux",
+ },
+ },
+ {
+ name: "invalid key-value format",
+ authHeader: "Signature foo:bar,baz",
+ expectedValues: map[string]string{},
+ },
+ {
+ name: "wrong authorization type",
+ authHeader: "Bearer foo=bar",
+ expectedValues: nil,
+ },
+ {
+ name: "missing auth header",
+ authHeader: "",
+ expectedValues: nil,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ header := http.Header{}
+ header.Set("Authorization", tc.authHeader)
+ require.Equal(t, tc.expectedValues, GetAuthorizationHeaderValues(header))
+ })
+ }
+}
+
+func TestCreateSignedRequest(t *testing.T) {
+ t.Parallel()
+
+ pemBytes, ok := fixtures.PEMBytes["rsa"]
+ require.True(t, ok)
+
+ provider := common.NewRawConfigurationProvider(
+ "ocid1.tenancy.oc1..abcd1234",
+ "ocid1.user.oc1..abcd1234",
+ "us-ashburn-1",
+ "fingerprint",
+ string(pemBytes),
+ nil,
+ )
+
+ innerHeader, outerHeader, err := createSignedRequest(provider, "challenge")
+ require.NoError(t, err)
+
+ expectedHeaders := map[string]string{
+ "Accept": "*/*",
+ "Authorization": "",
+ "Content-Length": "",
+ "Content-Type": "application/json",
+ "Host": "auth.us-ashburn-1.oraclecloud.com",
+ "User-Agent": teleportUserAgent,
+ "X-Content-Sha256": "",
+ DateHeader: "",
+ ChallengeHeader: "challenge",
+ }
+ expectedAuthHeader := map[string]string{
+ "version": "1",
+ "headers": "date (request-target) host x-date x-teleport-challenge content-length content-type x-content-sha256",
+ "keyId": "ocid1.tenancy.oc1..abcd1234/ocid1.user.oc1..abcd1234/fingerprint",
+ "algorithm": "rsa-sha256",
+ "signature": "",
+ }
+
+ for _, header := range []http.Header{innerHeader, outerHeader} {
+ for k, v := range expectedHeaders {
+ if v == "" {
+ assert.NotEmpty(t, header.Get(k), "missing header: %s", k)
+ } else {
+ assert.Equal(t, v, header.Get(k))
+ }
+ }
+ authValues := GetAuthorizationHeaderValues(header)
+ for k, v := range expectedAuthHeader {
+ if v == "" {
+ assert.NotEmpty(t, authValues[k], "missing auth header value: %s", k)
+ } else {
+ assert.Equal(t, v, authValues[k])
+ }
+ }
+ }
+}
+
+func TestFetchOraclePrincipalClaims(t *testing.T) {
+ t.Parallel()
+
+ defaultTenancyID := "tenancy-id"
+ defaultCompartmentID := "compartment-id"
+ defaultInstanceID := "instance-id"
+
+ defaultHandler := func(code int, responseBody any) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(code)
+ body, err := json.Marshal(responseBody)
+ assert.NoError(t, err)
+ _, err = w.Write(body)
+ assert.NoError(t, err)
+ })
+ }
+
+ tests := []struct {
+ name string
+ handler http.Handler
+ assert assert.ErrorAssertionFunc
+ expectedClaims Claims
+ }{
+ {
+ name: "ok",
+ handler: defaultHandler(http.StatusOK, authenticateClientResult{
+ Principal: principal{
+ Claims: []claim{
+ {
+ Key: tenancyClaim,
+ Value: defaultTenancyID,
+ },
+ {
+ Key: compartmentClaim,
+ Value: defaultCompartmentID,
+ },
+ {
+ Key: instanceClaim,
+ Value: defaultInstanceID,
+ },
+ },
+ },
+ }),
+ assert: assert.NoError,
+ expectedClaims: Claims{
+ TenancyID: defaultTenancyID,
+ CompartmentID: defaultCompartmentID,
+ InstanceID: defaultInstanceID,
+ },
+ },
+ {
+ name: "block redirect",
+ handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.NotEqual(t, "/dontgohere", r.RequestURI, "redirect was incorrectly performed")
+ http.Redirect(w, r, "/dontgohere", http.StatusFound)
+ }),
+ assert: assert.Error,
+ },
+ {
+ name: "http error",
+ handler: defaultHandler(http.StatusNotFound, authenticateClientResult{}),
+ assert: assert.Error,
+ },
+ {
+ name: "api error",
+ handler: defaultHandler(http.StatusOK, authenticateClientResult{
+ ErrorMessage: "it didn't work",
+ }),
+ assert: assert.Error,
+ },
+ {
+ name: "invalid response type",
+ handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("some junk"))
+ }),
+ assert: assert.Error,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ srv := httptest.NewServer(tc.handler)
+ t.Cleanup(srv.Close)
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+ req, err := http.NewRequest("", srv.URL, nil)
+ require.NoError(t, err)
+ claims, err := FetchOraclePrincipalClaims(ctx, req)
+ tc.assert(t, err)
+ assert.Equal(t, tc.expectedClaims, claims)
+ })
+ }
+}
+
+func TestParseRegion(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ inputRegion string
+ expectedRegion string
+ expectedRealm string
+ }{
+ {
+ name: "valid full region",
+ inputRegion: "us-phoenix-1",
+ expectedRegion: "us-phoenix-1",
+ expectedRealm: "oc1",
+ },
+ {
+ name: "valid abbreviated region",
+ inputRegion: "iad",
+ expectedRegion: "us-ashburn-1",
+ expectedRealm: "oc1",
+ },
+ {
+ name: "invalid region",
+ inputRegion: "foo",
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ region, realm := ParseRegion(tc.inputRegion)
+ assert.Equal(t, tc.expectedRegion, region)
+ assert.Equal(t, tc.expectedRealm, realm)
+ })
+ }
+}
+
+func TestParseRegionFromOCID(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ ocid string
+ assert assert.ErrorAssertionFunc
+ expectedRegion string
+ }{
+ {
+ name: "ok",
+ ocid: "ocid1.instance.oc1.us-phoenix-1.abcd1234",
+ assert: assert.NoError,
+ expectedRegion: "us-phoenix-1",
+ },
+ {
+ name: "ok with future use",
+ ocid: "ocid1.instance.oc1.us-phoenix-1.FUTURE.abcd1234",
+ assert: assert.NoError,
+ expectedRegion: "us-phoenix-1",
+ },
+ {
+ name: "ok with compartment/tenancy",
+ ocid: "ocid1.compartment.oc1..abcd1234",
+ assert: assert.NoError,
+ },
+ {
+ name: "not an ocid",
+ ocid: "some.junk",
+ assert: assert.Error,
+ },
+ {
+ name: "invalid version",
+ ocid: "ocid2.instance.oc1.us-phoenix-1.abcd1234",
+ assert: assert.Error,
+ },
+ {
+ name: "missing region on instance",
+ ocid: "ocid1.instance.oc1..abcd1234",
+ assert: assert.Error,
+ },
+ {
+ name: "unexpected region on compartment/tenancy",
+ ocid: "ocid1.tenancy.oc1.us-phoenix-1.abcd1234",
+ assert: assert.Error,
+ },
+ {
+ name: "invalid realm",
+ ocid: "ocid1.instance.ocxyz.us-phoenix-1.abcd1234",
+ assert: assert.Error,
+ },
+ {
+ name: "invalid region",
+ ocid: "ocid1.instance.oc1.junk-region.abcd1234",
+ assert: assert.Error,
+ },
+ {
+ name: "invalid region for realm",
+ ocid: "ocid1.instance.oc2.us-phoenix-1.abcd1234",
+ assert: assert.Error,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ region, err := ParseRegionFromOCID(tc.ocid)
+ tc.assert(t, err)
+ assert.Equal(t, tc.expectedRegion, region)
+ })
+ }
+}