diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..de15bbb0 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +REPO?=localhost:32000 +SKAFFOLD?=skaffold +KUBECTL?=kubectl + + +dev: + $(SKAFFOLD) run -p https + SKAFFOLD_DEFAULT_REPO=$(REPO) $(SKAFFOLD) run --port-forward --tail + + +info: + @echo create a client and then swap its credentials in the iam configmap diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..b607e63c --- /dev/null +++ b/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -x +# script requires the availability of rockcraft, skopeo, yq and docker in the host system + +# export version=$(yq -r '.version' rockcraft.yaml) +rockcraft pack -v + +skopeo --insecure-policy copy "oci-archive:hydra_$(yq -r '.version' rockcraft.yaml)_amd64.rock" docker-daemon:$IMAGE + +docker push $IMAGE diff --git a/hack/flow-test/Dockerfile b/hack/flow-test/Dockerfile new file mode 100644 index 00000000..228cbaa5 --- /dev/null +++ b/hack/flow-test/Dockerfile @@ -0,0 +1,30 @@ +FROM --platform=$BUILDPLATFORM golang:1.21-bullseye AS builder + +LABEL org.opencontainers.image.source=https://github.com/canonical/hydra-rock/hack/flow-test + +ARG SKAFFOLD_GO_GCFLAGS +ARG TARGETOS +ARG TARGETARCH + +ENV GOOS=$TARGETOS +ENV GOARCH=$TARGETARCH +ENV GO111MODULE=on +ENV CGO_ENABLED=1 + +WORKDIR /var/app + +COPY . . + +RUN go build -o app ./ + +# FROM gcr.io/distroless/static:nonroot +FROM gcr.io/distroless/base + + +LABEL org.opencontainers.image.source=https://github.com/canonical/hydra-rock/hack/flow-test + +WORKDIR / + +COPY --from=builder /var/app/app /app + +CMD ["/app"] diff --git a/hack/flow-test/go.mod b/hack/flow-test/go.mod new file mode 100644 index 00000000..72078e98 --- /dev/null +++ b/hack/flow-test/go.mod @@ -0,0 +1,26 @@ +module github.com/canonical/hydra-rock/hack/flow-test + +go 1.21.1 + +require ( + github.com/kelseyhightower/envconfig v1.4.0 + github.com/ory/hydra-client-go/v2 v2.1.1 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 + go.uber.org/zap v1.26.0 + golang.org/x/oauth2 v0.15.0 +) + +require ( + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/net v0.19.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/hack/flow-test/go.sum b/hack/flow-test/go.sum new file mode 100644 index 00000000..fff53985 --- /dev/null +++ b/hack/flow-test/go.sum @@ -0,0 +1,59 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/ory/hydra-client-go/v2 v2.1.1 h1:3JatU9uFbw5XhF3lgPCas1l1Kok2v5Mq1p26zZwGHNg= +github.com/ory/hydra-client-go/v2 v2.1.1/go.mod h1:IiIwChp/9wRvPoyFQblqPvg78uVishCCrV9+/M7Pl34= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/flow-test/main.go b/hack/flow-test/main.go new file mode 100644 index 00000000..b7278a72 --- /dev/null +++ b/hack/flow-test/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/kelseyhightower/envconfig" + client "github.com/ory/hydra-client-go/v2" + qrcode "github.com/skip2/go-qrcode" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.uber.org/zap" + oauth2 "golang.org/x/oauth2" + "golang.org/x/oauth2/github" +) + +// shipperizer in ~/shipperizer/hydra-rock/hack/flow-test on IAM-596 ● λ http http://127.0.0.1:4444/.well-known/openid-configuration +// HTTP/1.1 200 OK +// Cache-Control: private, no-cache, no-store, must-revalidate +// Content-Length: 1976 +// Content-Type: application/json; charset=utf-8 +// Date: Thu, 30 Nov 2023 12:56:08 GMT + +// { +// "authorization_endpoint": "https://localhost:4444/oauth2/auth", +// "backchannel_logout_session_supported": true, +// "backchannel_logout_supported": true, +// "claims_parameter_supported": false, +// "claims_supported": [ +// "sub" +// ], +// "code_challenge_methods_supported": [ +// "plain", +// "S256" +// ], +// "credentials_endpoint_draft_00": "https://localhost:4444/credentials", +// "credentials_supported_draft_00": [ +// { +// "cryptographic_binding_methods_supported": [ +// "jwk" +// ], +// "cryptographic_suites_supported": [ +// "PS256", +// "RS256", +// "ES256", +// "PS384", +// "RS384", +// "ES384", +// "PS512", +// "RS512", +// "ES512", +// "EdDSA" +// ], +// "format": "jwt_vc_json", +// "types": [ +// "VerifiableCredential", +// "UserInfoCredential" +// ] +// } +// ], +// "device_authorization_endpoint": "https://localhost:4444/oauth2/device/auth", +// "end_session_endpoint": "https://localhost:4444/oauth2/sessions/logout", +// "frontchannel_logout_session_supported": true, +// "frontchannel_logout_supported": true, +// "grant_types_supported": [ +// "authorization_code", +// "implicit", +// "client_credentials", +// "refresh_token", +// "urn:ietf:params:oauth:grant-type:device_code" +// ], +// "id_token_signed_response_alg": [ +// "RS256" +// ], +// "id_token_signing_alg_values_supported": [ +// "RS256" +// ], +// "issuer": "https://localhost:4444/", +// "jwks_uri": "https://localhost:4444/.well-known/jwks.json", +// "request_object_signing_alg_values_supported": [ +// "none", +// "RS256", +// "ES256" +// ], +// "request_parameter_supported": true, +// "request_uri_parameter_supported": true, +// "require_request_uri_registration": true, +// "response_modes_supported": [ +// "query", +// "fragment" +// ], +// "response_types_supported": [ +// "code", +// "code id_token", +// "id_token", +// "token id_token", +// "token", +// "token id_token code" +// ], +// "revocation_endpoint": "https://localhost:4444/oauth2/revoke", +// "scopes_supported": [ +// "offline_access", +// "offline", +// "openid" +// ], +// "subject_types_supported": [ +// "public" +// ], +// "token_endpoint": "https://localhost:4444/oauth2/token", +// "token_endpoint_auth_methods_supported": [ +// "client_secret_post", +// "client_secret_basic", +// "private_key_jwt", +// "none" +// ], +// "userinfo_endpoint": "https://localhost:4444/userinfo", +// "userinfo_signed_response_alg": [ +// "RS256" +// ], +// "userinfo_signing_alg_values_supported": [ +// "none", +// "RS256" +// ] +// } + +// shipperizer in ~/shipperizer/hydra on feat_dev_grants_2x λ ./hydra create client --endpoint http://127.0.0.1:4445 --name noauth --grant-type authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:device_code --response-type code,id_token --scope openid,offline --redirect-uri http://localhost:1337/hello --token-endpoint-auth-method none +// CLIENT ID 5a9d0eaf-2f78-4b66-b63d-49838ed33f19 +// CLIENT SECRET +// GRANT TYPES authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code +// RESPONSE TYPES code, id_token +// SCOPE openid offline +// AUDIENCE +// REDIRECT URIS http://localhost:1337/hello +// shipperizer in ~/shipperizer/hydra on feat_dev_grants_2x λ ./hydra create client --endpoint http://127.0.0.1:4445 --name auth --grant-type authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:device_code --response-type code,id_token --scope openid,offline --redirect-uri http://localhost:8000/api/ready +// CLIENT ID efc05555-e960-4aa2-bc0d-291144e15963 +// CLIENT SECRET cjMbxFlwS1BHXEyVpR-3JLoTPN +// GRANT TYPES authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code +// RESPONSE TYPES code, id_token +// SCOPE openid offline +// AUDIENCE +// REDIRECT URIS http://localhost:8000/api/ready + +type ProviderType int + +const ( + Hydra ProviderType = iota + Github +) + +// EnvSpec is the basic environment configuration setup needed for the app to start +type EnvSpec struct { + OAuthClientID string `envconfig:"oauth_client_id"` + OAuthClientSecret string `envconfig:"oauth_client_secret"` + CallbackURI string `envconfig:"callback_uri" default:"http://localhost:8000/api/ready"` + Scopes []string `envconfig:"scopes" default:"openid,email,profile,offline"` + Provider ProviderType `envconfig:"provider" default:"0"` + AuthURL string `envconfig:"auth_url" default:"http://localhost:4444/oauth2/auth"` + TokenURL string `envconfig:"token_url" default:"http://localhost:4444/oauth2/token"` + DeviceAuthURL string `envconfig:"device_auth_url" default:"http://localhost:4444/oauth2/device/auth"` + HydraAdminApiURL string `envconfig:"hydra_admin_api_url" default:"http://localhost:4445"` +} + +func registerHydraClient(hydraAdminUrl string, logger *zap.SugaredLogger) string { + configuration := client.NewConfiguration() + configuration.Servers = []client.ServerConfiguration{ + { + URL: hydraAdminUrl, + }, + } + + configuration.HTTPClient = &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} + + c := client.NewAPIClient(configuration) + + oauthClient := client.NewOAuth2Client() + oauthClient.SetGrantTypes([]string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"}) + oauthClient.SetScope("openid profile offline email offline_access") + oauthClient.SetTokenEndpointAuthMethod("none") + + cc, _, err := c.OAuth2Api.CreateOAuth2Client(context.Background()).OAuth2Client(*oauthClient).Execute() + if err != nil { + panic("Failed to create oauth2 client " + err.Error()) + } + logger.Infof("Created client, with client_id=%s", *cc.ClientId) + return *cc.ClientId +} + +func deviceFlow(specs *EnvSpec, logger *zap.SugaredLogger) { + config := new(oauth2.Config) + config.ClientID = specs.OAuthClientID + config.ClientSecret = specs.OAuthClientSecret + config.Scopes = specs.Scopes + + switch specs.Provider { + case Github: + config.Endpoint = github.Endpoint + case Hydra: + config.Endpoint = oauth2.Endpoint{ + AuthURL: specs.AuthURL, + TokenURL: specs.TokenURL, + DeviceAuthURL: specs.DeviceAuthURL, + AuthStyle: oauth2.AuthStyleInHeader, + } + } + + for { + ctx := context.Background() + + response, err := config.DeviceAuth(ctx) + + if err != nil { + logger.Errorf(err.Error()) + logger.Warn("sleeping for 60s") + time.Sleep(60 * time.Second) + continue + } + + logger.Debugf("response: %v", response) + logger.Infof( + ` +############################################################ +add the following to your /etc/hosts +"127.0.0.1 iam.internal" +############################################################ +run the following command: $(KUBECTL) get secret -o yaml iam-tls | yq '.data' +copy the ca.crt and tls.crt into /usr/local/share/ca-certificates/ and run update-ca-certificates +to get those certs added to the system pool (and trust them), you might need to do +the same (trust) in your chrome/firefox/safari browser +after that you should be able to point openssl or certigo to the forwarded ingress on your localhost (port 8443) and +verify that the cert is valid +############################################################ +please enter code %s at %s +or go to %s +############################################################ + `, + response.UserCode, + response.VerificationURI, + response.VerificationURIComplete, + ) + if qr, err := qrcode.New(response.VerificationURIComplete, qrcode.Low); err == nil { + logger.Infof("############################################################") + logger.Infof("or scan this %s", qr.ToString(true)) + logger.Infof("############################################################") + } + token, err := config.DeviceAccessToken(ctx, response) + + if err != nil { + logger.Warn(err, token) + } else { + logger.Infof("You are logged in") + logger.Infof("Access Token: %s", token.AccessToken) + logger.Infof("Refresh Token: %s", token.RefreshToken) + logger.Infof("ID Token: %s", token.Extra("id_token")) + } + + logger.Info("device flow done...one way or the other") + logger.Infof("############################################################") + break + } +} + +// example taken from https://github.com/supercairos/oauth-device-flow-client-sample/blob/master/src/index.ts +func main() { + var logger *zap.SugaredLogger + + if _log, err := zap.NewDevelopment(); err != nil { + logger = zap.NewNop().Sugar() + } else { + logger = _log.Sugar() + } + + specs := new(EnvSpec) + + if err := envconfig.Process("", specs); err != nil { + panic(fmt.Errorf("issues with environment sourcing: %s", err)) + } + + if specs.OAuthClientID == "" { + specs.OAuthClientID = registerHydraClient(specs.HydraAdminApiURL, logger) + } + + go deviceFlow(specs, logger) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + // Block until we receive our signal. + <-c + + logger.Desugar().Sync() + + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + logger.Info("Shutting down") + os.Exit(0) +} diff --git a/hack/helm/hydra.yaml b/hack/helm/hydra.yaml new file mode 100644 index 00000000..42234cdf --- /dev/null +++ b/hack/helm/hydra.yaml @@ -0,0 +1,228 @@ +hydra: + dev: "true" + config: + log: + level: debug + leak_sensitive_values: true + serve: + cookies: + same_site_mode: Lax + dsn: "postgres://iam:iam@postgresql.default.svc.cluster.local/hydra?sslmode=disable&max_conn_lifetime=10s" + # dsn: memory + secrets: + system: + - SUFNUGxhdGZvcm0K + webfinger: + jwks: + broadcast_keys: + - hydra.openid.id-token + oidc_discovery: + jwks_url: https://iam.internal:8443/.well-known/jwks.json + auth_url: https://iam.internal:8443/oauth2/auth + token_url: https://iam.internal:8443/oauth2/token + device_authorization_url: https://iam.internal:8443/oauth2/device/auth + urls: + self: + issuer: https://iam.internal:8443/ + public: https://iam.internal:8443/ + login: https://iam.internal:8443/ui/login + consent: https://iam.internal:8443/ui/consent + device_verification: https://iam.internal:8443/ui/device_code + post_device_done: https://iam.internal:8443/api/v0/status + error: https://iam.internal:8443/ui/oidc_error + ttl: + # configures how long a user login and consent flow may take. Defaults to 1h. + login_consent_request: 1h + # configures how long access tokens are valid. Defaults to 1h. + access_token: 1h + # configures how long refresh tokens are valid. Defaults to 720h. Set to -1 for refresh tokens to never expire. + refresh_token: 720h + # configures how long id tokens are valid. Defaults to 1h. + id_token: 1h + # configures how long auth codes are valid. Defaults to 10m. + auth_code: 30m + # configures how long device and user codes are valid. Defaults to 10m. + device_user_code: 30m + oauth2: + # Set this to true if you want to share error debugging information with your OAuth 2.0 clients. + # Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error + # codes and similar errors. Defaults to false. + expose_internal_errors: true + # Configures hashing algorithms. Supports only BCrypt at the moment. + hashers: + # Configures the BCrypt hashing algorithm used for hashing Client Secrets. + bcrypt: + # Sets the BCrypt cost. Minimum value is 4 and default value is 10. The higher the value, the more CPU time is being + # used to generate hashes. + cost: 10 + pkce: + # Set this to true if you want PKCE to be enforced for all clients. + enforced: false + # Set this to true if you want PKCE to be enforced for public clients. + enforced_for_public_clients: false + session: + # store encrypted data in database, default true + encrypt_at_rest: true + device_authorization: + # configure how often a non-interactive device should poll the device token endpoint, default 5s + token_polling_interval: 5s + automigration: + enabled: true +maester: + enabled: false + +# https://github.com/ory/hydra/pull/3252/files -- internal/config/config.yaml +# log: +# level: debug +# leak_sensitive_values: false +# format: json + +# serve: +# public: +# port: 1 +# host: localhost +# socket: +# owner: hydra +# group: hydra-public-api +# mode: 0775 +# cors: +# enabled: false +# allowed_origins: +# - https://example.com +# allowed_methods: +# - GET +# allowed_headers: +# - Authorization +# exposed_headers: +# - Content-Type +# allow_credentials: true +# max_age: 1 +# debug: false +# request_log: +# disable_for_health: false +# admin: +# port: 2 +# host: localhost +# socket: +# owner: hydra +# group: hydra-admin-api +# mode: 0770 + +# cors: +# enabled: false +# allowed_origins: +# - https://example.com +# allowed_methods: +# - GET +# allowed_headers: +# - Authorization +# exposed_headers: +# - Content-Type +# allow_credentials: true +# max_age: 1 +# debug: false +# request_log: +# disable_for_health: false +# tls: +# key: +# path: /path/to/file.pem +# cert: +# base64: b3J5IGh5ZHJhIGlzIGF3ZXNvbWUK +# allow_termination_from: +# - 127.0.0.1/32 +# cookies: +# same_site_mode: Lax +# same_site_legacy_workaround: true + +# dsn: memory + +# hsm: +# enabled: false + +# webfinger: +# jwks: +# broadcast_keys: +# - hydra.openid.id-token +# oidc_discovery: +# jwks_url: https://example.com/jwks.json +# auth_url: https://example.com/auth +# token_url: https://example.com/token +# client_registration_url: https://example.com +# device_authorization_url: https://example.com/device_authorization +# supported_claims: +# - username +# supported_scope: +# - whatever +# userinfo_url: https://example.com + +# oidc: +# subject_identifiers: +# supported_types: +# - pairwise +# pairwise: +# salt: random_salt +# dynamic_client_registration: +# enabled: false +# default_scope: +# - whatever + +# urls: +# self: +# issuer: https://issuer +# public: https://public +# admin: https://admin +# login: https://login +# consent: https://consent +# logout: https://logout +# device: https://device +# error: https://error +# post_device_done: https://post_device +# post_logout_redirect: https://post_logout + +# strategies: +# scope: exact +# access_token: opaque + +# ttl: +# login_consent_request: 2h +# access_token: 2h +# refresh_token: 2h +# id_token: 2h +# auth_code: 2h +# device_user_code: 2h + +# oauth2: +# expose_internal_errors: true +# device_authorization: +# token_polling_interval: 2h +# hashers: +# bcrypt: +# cost: 20 +# pkce: +# enforced: true +# enforced_for_public_clients: true + +# secrets: +# system: +# - some-random-system-secret +# cookie: +# - some-random-cookie-secret + +# profiling: cpu + +# tracing: +# provider: jaeger +# service_name: hydra service +# providers: +# jaeger: +# local_agent_address: 127.0.0.1:6831 +# sampling: +# trace_id_ratio: 1 +# server_url: http://sampling +# zipkin: +# server_url: http://zipkin/api/v2/spans +# otlp: +# insecure: true +# server_url: localhost:4318 +# sampling: +# sampling_ratio: 1.0 \ No newline at end of file diff --git a/hack/helm/kratos.yaml b/hack/helm/kratos.yaml new file mode 100644 index 00000000..f40af92a --- /dev/null +++ b/hack/helm/kratos.yaml @@ -0,0 +1,205 @@ +kratos: + development: true + config: + log: + level: debug + format: text + leak_sensitive_values: true + serve: + public: + base_url: https://iam.internal:8443 + cors: + enabled: true + admin: + base_url: http://kratos-admin.default.svc.cluster.local + dsn: "postgres://iam:iam@postgresql.default.svc.cluster.local/kratos?sslmode=disable&max_conn_lifetime=10s" + secrets: + default: + - SUFNUGxhdGZvcm0K + cookie: + - fmtGCN1S4euicZhOi8bBtkdeDIrRL5KZ + identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/identity.schema.json + courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true + oauth2_provider: + url: http://hydra-admin.default.svc.cluster.local:4445 + selfservice: + allowed_return_urls: + - https://iam.internal:8443 + default_browser_return_url: + https://iam.internal:8443 + flows: + error: + ui_url: https://iam.internal:8443/ui/error + login: + ui_url: https://iam.internal:8443/ui/login + registration: + after: + oidc: + hooks: + - hook: session + methods: + password: + enabled: False + oidc: + enabled: True + config: + providers: + - id: "microsoft" + provider: "microsoft" + mapper_url: "file:///etc/config/microsoft_schema.jsonnet" + scope: ["profile", "email", "address", "phone"] + client_id: "***********************" + client_secret: "***********************" + microsoft_tenant: "***********************" + - id: "github" + provider: "github" + mapper_url: "file:///etc/config/github_schema.jsonnet" + scope: ["user:email"] + client_id: "***********************" + client_secret: "***********************" + automigration: + enabled: true + identitySchemas: + "microsoft_schema.jsonnet": | + local claims = std.extVar('claims'); + + { + identity: { + traits: { + [if 'email' in claims then 'email' else null]: claims.email, + [if 'name' in claims then 'name' else null]: claims.name, + [if 'given_name' in claims then 'given_name' else null]: claims.given_name, + [if 'family_name' in claims then 'family_name' else null]: claims.family_name, + [if 'last_name' in claims then 'last_name' else null]: claims.last_name, + [if 'middle_name' in claims then 'middle_name' else null]: claims.middle_name, + [if 'nickname' in claims then 'nickname' else null]: claims.nickname, + [if 'preferred_username' in claims then 'preferred_username' else null]: claims.preferred_username, + [if 'profile' in claims then 'profile' else null]: claims.profile, + [if 'picture' in claims then 'picture' else null]: claims.picture, + [if 'website' in claims then 'website' else null]: claims.website, + [if 'gender' in claims then 'gender' else null]: claims.gender, + [if 'birthdate' in claims then 'birthdate' else null]: claims.birthdate, + [if 'zoneinfo' in claims then 'zoneinfo' else null]: claims.zoneinfo, + [if 'phone_number' in claims && claims.phone_number_verified then 'phone_number' else null]: claims.phone_number, + [if 'locale' in claims then 'locale' else null]: claims.locale, + [if 'team' in claims then 'team' else null]: claims.team, + }, + }, + } + "github_schema.jsonnet": | + local claims = std.extVar('claims'); + + { + identity: { + traits: { + [if 'email' in claims then 'email' else null]: claims.email, + [if 'name' in claims then 'name' else null]: claims.name, + [if 'given_name' in claims then 'given_name' else null]: claims.given_name, + [if 'family_name' in claims then 'family_name' else null]: claims.family_name, + [if 'last_name' in claims then 'last_name' else null]: claims.last_name, + [if 'middle_name' in claims then 'middle_name' else null]: claims.middle_name, + [if 'nickname' in claims then 'nickname' else null]: claims.nickname, + [if 'preferred_username' in claims then 'preferred_username' else null]: claims.preferred_username, + [if 'profile' in claims then 'profile' else null]: claims.profile, + [if 'picture' in claims then 'picture' else null]: claims.picture, + [if 'website' in claims then 'website' else null]: claims.website, + [if 'gender' in claims then 'gender' else null]: claims.gender, + [if 'birthdate' in claims then 'birthdate' else null]: claims.birthdate, + [if 'zoneinfo' in claims then 'zoneinfo' else null]: claims.zoneinfo, + [if 'phone_number' in claims && claims.phone_number_verified then 'phone_number' else null]: claims.phone_number, + [if 'locale' in claims then 'locale' else null]: claims.locale, + [if 'team' in claims then 'team' else null]: claims.team, + }, + }, + } + "identity.schema.json": | + { + "$id": "https://schemas.canonical.com/presets/kratos/user_v0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "given_name": { + "type": "string", + "title": "Given Name" + }, + "family_name": { + "type": "string", + "title": "Family Name" + }, + "last_name": { + "type": "string", + "title": "Last Name" + }, + "middle_name": { + "type": "string", + "title": "Middle Name" + }, + "nickname": { + "type": "string", + "title": "Nickname" + }, + "preferred_username": { + "type": "string", + "title": "Preferred Username" + }, + "profile": { + "type": "string", + "title": "Profile" + }, + "picture": { + "type": "string", + "title": "Picture" + }, + "website": { + "type": "string", + "title": "Website" + }, + "email": { + "type": "string", + "format": "email", + "title": "E-Mail" + }, + "gender": { + "type": "string", + "title": "Gender" + }, + "birthdate": { + "type": "string", + "title": "Birthdate" + }, + "zoneinfo": { + "type": "string", + "title": "Zoneinfo" + }, + "locale": { + "type": "string", + "title": "Locale" + }, + "phone_number": { + "type": "string", + "title": "Phone Number" + }, + "address": { + "type": "string", + "title": "Address" + } + }, + "required": ["email"] + }, + "additionalProperties": true + } + } diff --git a/hack/helm/postgresql.yaml b/hack/helm/postgresql.yaml new file mode 100644 index 00000000..3aedd9eb --- /dev/null +++ b/hack/helm/postgresql.yaml @@ -0,0 +1,19 @@ +global: + postgresql: + auth: + database: "iam" + username: "iam" + password: "iam" + postgresPassword: "iam" +primary: + initdb: + scripts: + init.sql: | + CREATE DATABASE hydra; + CREATE DATABASE kratos; + +auth: + database: "iam" + username: "iam" + password: "iam" + postgresPassword: "iam" \ No newline at end of file diff --git a/hack/kubectl/ca.yaml b/hack/kubectl/ca.yaml new file mode 100644 index 00000000..60b9260c --- /dev/null +++ b/hack/kubectl/ca.yaml @@ -0,0 +1,66 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: selfsigned-ca +spec: + isCA: true + commonName: selfsigned-ca + secretName: root-secret + privateKey: + algorithm: ECDSA + size: 256 + issuerRef: + name: selfsigned-issuer + kind: ClusterIssuer + group: cert-manager.io +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: ca-issuer +spec: + ca: + secretName: root-secret +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: iam + namespace: default +spec: + subject: + organizations: + - canonical + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + - client auth + # At least one of a DNS Name, URI, or IP address is required. + dnsNames: + - iam.internal + - iam.default.svc.cluster.local + - kratos-public.default.svc.cluster.local + - kratos-admin.default.svc.cluster.local + - hydra-public.default.svc.cluster.local + - hydra-admin.default.svc.cluster.local + # ipAddresses: + # - 192.168.0.5 + # Issuer references are always required. + issuerRef: + kind: ClusterIssuer + name: selfsigned-issuer + duration: 2160h # 90d + renewBefore: 360h # 15d + secretName: iam-tls + diff --git a/hack/kubectl/flow-test.yaml b/hack/kubectl/flow-test.yaml new file mode 100644 index 00000000..3ecfed7b --- /dev/null +++ b/hack/kubectl/flow-test.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: flow-test + name: flow-test +spec: + replicas: 1 + selector: + matchLabels: + app: flow-test + strategy: + type: Recreate + template: + metadata: + labels: + app: flow-test + spec: + containers: + - name: flow-test + image: flow-test + command: ["/app"] + envFrom: + - configMapRef: + name: flow-test-hydra + # - configMapRef: + # name: flow-test-github + volumeMounts: + - name: tls + mountPath: "/ssl/certs/iam.crt" + subPath: tls.crt + readOnly: true + - name: tls + mountPath: "/ssl/certs/iam.ca.crt" + subPath: ca.crt + readOnly: true + volumes: + - name: tls + secret: + secretName: iam-tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: flow-test + name: flow-test +spec: + ports: + - name: web + port: 80 + targetPort: 8000 + selector: + app: flow-test +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: flow-test-hydra +data: + HYDRA_ADMIN_API_URL: http://hydra-admin.default.svc.cluster.local:4445 + CALLBACK_URI: http://flow-test.default.svc.cluster.local/api/ready + # OAUTH_CLIENT_SECRET: KOVbs8SDizELCUQhFEDLYPA4zN + SCOPES: openid,offline,email,profile + PROVIDER: "0" # hydra + AUTH_URL: http://hydra-public.default.svc.cluster.local:4444/oauth2/auth + TOKEN_URL: http://hydra-public.default.svc.cluster.local:4444/oauth2/token + DEVICE_AUTH_URL: http://hydra-public.default.svc.cluster.local:4444/oauth2/device/auth +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: flow-test-github +data: + AUTH_CLIENT_SECRET: "abcdefghi" + OAUTH_CLIENT_ID: "abcdefghi" + SCOPES: user,repo + PROVIDER: "1" # github diff --git a/hack/kubectl/iam.yaml b/hack/kubectl/iam.yaml new file mode 100644 index 00000000..117d01e9 --- /dev/null +++ b/hack/kubectl/iam.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: iam + name: iam +spec: + replicas: 1 + selector: + matchLabels: + app: iam + strategy: + type: Recreate + template: + metadata: + labels: + app: iam + spec: + containers: + - name: iam + image: identity-platform-login-ui + envFrom: + - configMapRef: + name: iam +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: iam + name: iam +spec: + ports: + - name: web + port: 8000 + targetPort: 8000 + selector: + app: iam +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: iam +data: + TRACING_ENABLED: "false" + BASE_URL: https://iam.internal:8443 + KRATOS_PUBLIC_URL: http://kratos-public.default.svc.cluster.local + HYDRA_ADMIN_URL: http://hydra-admin.default.svc.cluster.local:4445 + PORT: "8000" + LOG_LEVEL: "info" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: iam + labels: + app: iam +spec: + tls: + - hosts: + - iam.internal + secretName: iam-tls + defaultBackend: + service: + name: iam + port: + number: 8000 + rules: + - host: iam.internal + http: + paths: + - pathType: Prefix + path: "/ui/" + backend: + service: + name: iam + port: + number: 8000 + - pathType: Prefix + path: "/api/device" + backend: + service: + name: iam + port: + number: 8000 + - pathType: Prefix + path: "/api/kratos" + backend: + service: + name: iam + port: + number: 8000 + - pathType: Exact + path: "/api/consent" + backend: + service: + name: iam + port: + number: 8000 + - pathType: Prefix + path: "/self-service/" + backend: + service: + name: kratos-public + port: + number: 80 + - pathType: Prefix + path: "/sessions/" + backend: + service: + name: kratos-public + port: + number: 80 + - pathType: Prefix + path: "/oauth2/" + backend: + service: + name: hydra-public + port: + number: 4444 + - pathType: Exact + path: "/.well-known/jwks.json" + backend: + service: + name: hydra-public + port: + number: 4444 + - host: iam.internal + http: + paths: + - pathType: Prefix + path: "/admin/clients" + backend: + service: + name: hydra-admin + port: + number: 4445 + - pathType: Prefix + path: "/admin/keys" + backend: + service: + name: hydra-admin + port: + number: 4445 + - pathType: Prefix + path: "/admin/oauth2" + backend: + service: + name: hydra-admin + port: + number: 4445 + - pathType: Prefix + path: "/admin/identities" + backend: + service: + name: kratos-admin + port: + number: 80 + - pathType: Prefix + path: "/admin/sessions" + backend: + service: + name: kratos-admin + port: + number: 80 diff --git a/hack/login-ui/build.sh b/hack/login-ui/build.sh new file mode 100755 index 00000000..f0ea24db --- /dev/null +++ b/hack/login-ui/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -x +# script requires the availability of rockcraft, skopeo, yq and docker in the host system + +export version=$(yq -r '.version' rockcraft.yaml) +rockcraft pack -v + +skopeo --insecure-policy copy "oci-archive:identity-platform-login-ui_$(yq -r '.version' rockcraft.yaml)_amd64.rock" docker-daemon:$IMAGE + +docker push $IMAGE diff --git a/hack/login-ui/rockcraft.yaml b/hack/login-ui/rockcraft.yaml new file mode 100644 index 00000000..6ace8ced --- /dev/null +++ b/hack/login-ui/rockcraft.yaml @@ -0,0 +1,44 @@ +name: identity-platform-login-ui + +base: bare +build-base: ubuntu@22.04 +version: '0.12.0' # x-release-please-version +summary: Canonical Identity platform login UI +description: | + This is the Canonical Identity platform login UI used for connecting + Ory Kratos with Ory Hydra. +license: Apache-2.0 + +platforms: + amd64: + +services: + login-ui: + override: replace + command: /usr/bin/identity-platform-login-ui serve + startup: enabled + +parts: + certificates: + plugin: nil + stage-packages: + - ca-certificates + + go-build: + plugin: go + source: https://github.com/canonical/identity-platform-login-ui + source-type: git + source-branch: IAM-736 + build-snaps: + - go/1.21/stable + - node/18/stable + build-packages: + - make + - git + override-build: | + make npm-build build + install -D -m755 app ${CRAFT_PART_INSTALL}/opt/identity-platform-login-ui/bin/app + organize: + opt/identity-platform-login-ui/bin/app: usr/bin/identity-platform-login-ui + stage-packages: + - base-files_var diff --git a/rockcraft.yaml b/rockcraft.yaml index e546137a..39a7ac5d 100644 --- a/rockcraft.yaml +++ b/rockcraft.yaml @@ -1,7 +1,7 @@ name: hydra base: bare build-base: ubuntu@22.04 -version: "2.2.0" +version: "2.x.xbeta" summary: Ory Hydra description: | Ory Hydra is a hardened and certified OAuth 2.0 and OpenID Connect provider. @@ -41,7 +41,7 @@ parts: build-snaps: - go/1.21/stable override-build: | - src_config_path="github.com/ory/hydra/v2/driver" + src_config_path="github.com/canonical/hydra/v2/driver" build_ver="${src_config_path}/config.Version" build_hash="${src_config_path}/config.Commit" build_date="${src_config_path}/config.Date" @@ -54,6 +54,6 @@ parts: export CGO_ENABLED=0 go mod download all go build -ldflags="${go_linker_flags}" -o "${CRAFT_PART_INSTALL}"/bin/hydra - source: https://github.com/ory/hydra + source: https://github.com/canonical/hydra source-type: git - source-tag: v2.2.0 + source-branch: canonical diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 00000000..09248d6c --- /dev/null +++ b/skaffold.yaml @@ -0,0 +1,119 @@ +apiVersion: skaffold/v4beta6 +kind: Config +build: + artifacts: + - image: "identity-platform-login-ui" + context: hack/login-ui + sync: + infer: + - rockcraft.yaml + custom: + buildCommand: ./build.sh + dependencies: + paths: + - rockcraft.yaml + platforms: ["linux/amd64"] + - image: "flow-test" + context: hack/flow-test + docker: + dockerfile: Dockerfile + platforms: ["linux/amd64"] + - image: "hydra" + sync: + infer: + - rockcraft.yaml + custom: + buildCommand: ./build.sh + dependencies: + paths: + - rockcraft.yaml + platforms: ["linux/amd64"] + local: + push: true + +manifests: + rawYaml: + - hack/kubectl/iam.yaml + - hack/kubectl/flow-test.yaml + +deploy: + helm: + releases: + - name: postgresql + remoteChart: oci://registry-1.docker.io/bitnamicharts/postgresql + valuesFiles: ["hack/helm/postgresql.yaml"] + - name: hydra + remoteChart: hydra + repo: https://k8s.ory.sh/helm/charts + wait: false + upgradeOnChange: true + valuesFiles: ["hack/helm/hydra.yaml"] + setValueTemplates: + image.repository: "{{.IMAGE_REPO_hydra}}" + image.tag: "{{.IMAGE_TAG_hydra}}@{{.IMAGE_DIGEST_hydra}}" + - name: kratos + remoteChart: kratos + repo: https://k8s.ory.sh/helm/charts + valuesFiles: ["hack/helm/kratos.yaml"] + wait: false + upgradeOnChange: true + +profiles: + - name: https + requiresAllActivations: false + patches: + - op: remove + path: /build/artifacts + manifests: + rawYaml: + - hack/kubectl/ca.yaml + deploy: + helm: + releases: + - name: cert-manager + repo: https://charts.jetstack.io + remoteChart: cert-manager + setValues: + installCRDs: true + wait: false + - name: contour + remoteChart: oci://registry-1.docker.io/bitnamicharts/contour + version: 15.0.1 + wait: false + +portForward: + - resourceType: service + resourceName: hydra-public + namespace: default + port: 4444 + localPort: 4444 + - resourceType: service + resourceName: hydra-admin + namespace: default + port: 4445 + localPort: 4445 + - resourceType: service + resourceName: kratos-admin + namespace: default + port: 80 + localPort: 4455 + - resourceType: service + resourceName: kratos-public + namespace: default + port: 80 + localPort: 4433 + - resourceType: service + resourceName: iam + namespace: default + port: 8000 + localPort: 8000 + - resourceType: service + resourceName: contour-envoy + namespace: default + port: 443 + localPort: 8443 + - resourceType: service + resourceName: contour-envoy + namespace: default + port: 80 + localPort: 8080