fluffycore-rage-identity

module
v1.0.13-herb-2fa-v2.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2024 License: MIT

README

FluffyCore Identity

A simple and opinionated OIDC Authentication Service

alt text This is a Proof-Of-Life authentication server.

The use case for this proof is the github.com user experience.

  1. A user is a stand-alone entity.
  2. A user can be linked in N number of external IDPs.
  3. A user can be challenged at any time against any known IDP , and the id_token must contain what idp (external or the root) wence the identity was produced.
  4. External IDPs are secret. We don't want anyone to know what external enterprises a user can be linked to.

No calls to the userinfo endpoint are supported. id_token is the only thing returned that is useful. It is meant to use that id_token as an argument to an internal token_exchange that knows more about the user in the context of that system.

TL;DR

Just build and run this thing;

 docker build --file .\build\Dockerfile . --tag fluffycore.rage.oidc:latest
 docker-compose up -d

Now that we have the server running in docker, lets run our client locally.

cd cmd/go-client
go build .

$env:PORT = "5556";$env:OAUTH2_CLIENT_ID = "go-client";$env:OAUTH2_CLIENT_SECRET = "secret";$env:AUTHORITY = "http://localhost:9044/"; .\go-client.exe

Open your browser, Edge is best and we all know it!

Navigate to http://localhost:5556/login

Any username and password will work.

You should see a json response like this.

{
  "OAuth2Token": {
    "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjBiMmNkMmU1NGM5MjRjZTg5ZjAxMGYyNDI4NjIzNjdkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJnby1jbGllbnQiLCJjbGllbnRfaWQiOiJnby1jbGllbnQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJleHAiOjE3MDc1MjQ0ODUsImlhdCI6MTcwNzUyMDg4NSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDQ0IiwianRpIjoiY24zYjZ0YWkycW5iMzc4MjgwbjAiLCJuYmYiOjE3MDc1MjA1ODUsInBlcm1pc3Npb25zIjpbInJlYWQiLCJ3cml0ZSJdLCJzdWIiOiIxMjMifQ.R9zQX2njveB-iUhQTO698logMjPtFdDbe7Ne2scSoT8kcMEMk3wEIz2D8tyzcjSlsqSSoXoAP6YKo1dIfnFOOQ",
    "token_type": "bearer",
    "refresh_token": "refresh_token",
    "expiry": "2024-02-09T16:21:26.0012199-08:00"
  },
  "IDTokenClaims": {
    "aud": "go-client",
    "client_id": "go-client",
    "email": "test@test.com",
    "exp": 1707524485,
    "iat": 1707520885,
    "iss": "http://localhost:9044",
    "jti": "cn3b6tai2qnb378280mg",
    "nbf": 1707520585,
    "nonce": "AeJpC-NrPt0ED3-Qh2M34g",
    "sub": "123"
  },
  "IDToken": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjBiMmNkMmU1NGM5MjRjZTg5ZjAxMGYyNDI4NjIzNjdkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJnby1jbGllbnQiLCJjbGllbnRfaWQiOiJnby1jbGllbnQiLCJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJleHAiOjE3MDc1MjQ0ODUsImlhdCI6MTcwNzUyMDg4NSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDQ0IiwianRpIjoiY24zYjZ0YWkycW5iMzc4MjgwbWciLCJuYmYiOjE3MDc1MjA1ODUsIm5vbmNlIjoiQWVKcEMtTnJQdDBFRDMtUWgyTTM0ZyIsInN1YiI6IjEyMyJ9.0KuxDAlXX4DIh5Lh0KSXTahY8gQicRYVWMd-4Ic8J5ZwbFwnrFPk_sgE2cGetcaAFiReHg1SYAszsY8Sahds6A"
}

Protos

Note: I had to run bash on windows so I could pass ./api/proto/**/*.proto

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/fluffy-bunny/fluffycore/protoc-gen-go-fluffycore-di/cmd/protoc-gen-go-fluffycore-di@latest

go get github.com/fluffy-bunny/fluffycore

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/helloworld/helloworld.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/types/primitives.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/types/filter.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/types/pagination.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/types/phone_number.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/models/client.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/models/idp.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/client/client.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/idp/idp.proto

protoc --go_out=. --go_opt paths=source_relative  --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/models/user.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/user/user.proto

protoc --go_out=. --go_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/oidc/flows/oidc_flow.proto

protoc --go_out=. --go_opt paths=source_relative  --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/external/models/user.proto

protoc --go_out=. --go_opt paths=source_relative  --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/external/user/user.proto

protoc --go_out=. --go_opt paths=source_relative  --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --openapiv2_out=allow_merge=true,merge_file_name=proto:./proto --go-grpc_out . --go-grpc_opt paths=source_relative --go-fluffycore-di_out .  --go-fluffycore-di_opt paths=source_relative,grpc_gateway=true  ./proto/types/webauthn/webauthn.proto

Private OAuth2 server

The kit comes with a self contained oauth2 server.

Your apis need tokens, and here we can define exactly what claims a given client will mint.

The client_credenitials flow is the only thing supported.

discovery
jwks

client_credentials example:

curl --location 'http://localhost:50053/oauth/token' --header 'Content-Type: application/x-www-form-urlencoded' --header 'Authorization: Basic Y2xpZW50MTpzZWNyZXQ=' --data-urlencode 'grant_type=client_credentials'
{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjBiMmNkMmU1NGM5MjRjZTg5ZjAxMGYyNDI4NjIzNjdkIiwidHlwIjoiSldUIn0.eyJjbGllbnRfaWQiOiJjbGllbnQxIiwiZXhwIjoxNjk5MjI3MzY3LCJpYXQiOjE2OTkyMjM3NjcsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwNTMiLCJwZXJtaXNzaW9ucyI6WyJyZWFkIiwid3JpdGUiXSwic3ViIjoiY2xpZW50MSJ9.hAtAa5W81NATUZmNDVQdQLYSmA_0Wx4HvmSMOcqGMdQMS7ay99v1RmKf-kT2l8Xm6rDMG8klIiEU9M-FK-400w",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Docker Build

 docker build --file .\build\Dockerfile . --tag fluffycore.rage.oidc:latest

Health check

go-healthcheck

COPY --from=gregthebunny/go-healthcheck /bin/healthcheck /bin/healthcheck
ENV PROBE='{{ .Assert.HTTPBodyContains .HTTP.Handler "GET" "http://localhost:50052/healthz" nil "SERVING" }}'
HEALTHCHECK --start-period=10s --retries=3 --timeout=10s --interval=10s \
CMD ["/bin/healthcheck", "probe", "$PROBE"]

Now all that is needed for another service to check health is a condition: service_healthy

whoami:
  container_name: whoami
  extends:
    file: ./docker-compose-common.yml
    service: micro
  image: containous/whoami
  security_opt:
    - no-new-privileges:true
  depends_on:
    starterkit:
      condition: service_healthy

Docker Compose

docker-compose -f .\docker-compose.yml up -d

Swagger

echo-swagger

We want to swag init the general dir first, which is in the cmd/server directory. Then we want to include the swaggers in the internal

cd cmd/server
swag init  --dir ./,../../internal

GO OIDC CLIENT

cd cmd/go-client
go build .

$env:PORT = "5556";$env:OAUTH2_CLIENT_ID = "go-client";$env:OAUTH2_CLIENT_SECRET = "secret";$env:AUTHORITY = "http://localhost:9044"; .\go-client.exe

$env:PORT = "5556";$env:OAUTH2_CLIENT_ID = "go-client";$env:OAUTH2_CLIENT_SECRET = "secret";$env:AUTHORITY = "http://localhost:9044"; .\go-client.exe
Dev Client
cd cmd/oidc-client
go build .

.\oidc-client.exe serve    --authority http://localhost:9044 --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --acr_values "urn:rage:idp:google-social"   --authority http://localhost:9044 --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --acr_values "urn:rage:idp:mapped-enterprise" --acr_values "urn:rage:root_candidate:cnf07331og1ecp4r680g"  --authority http://localhost:9044 --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --acr_values "urn:rage:idp:mapped-enterprise"  --authority http://localhost:9044 --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --authority https://3156-47-150-126-75.ngrok-free.app --client_id go-client --client_secret secret --port 5556

Docker Clients
.\oidc-client.exe serve    --authority https://rage.localhost.dev --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --acr_values "urn:rage:idp:mapped-enterprise"   --authority https://rage.localhost.dev --client_id go-client --client_secret secret --port 5556

.\oidc-client.exe serve --acr_values "urn:rage:idp:mapped-enterprise" --acr_values "urn:rage:root_candidate:cnf08ok1fnuu73eq91vg"   --authority https://rage.localhost.dev --client_id go-client --client_secret secret --port 5556


PassKeys

For developement we need https. This is where ngrok comes in.

NOTE: Because we use ngrok we don't have a stable domain. So all IDP logins will fail, because we need to register a stable https domain with google, github, microsoft, azure, etc.

Passkey development can only work for simple username/password accounts.

ngrok http http://localhost:9044

This will give you your ngrok url.

ngrok
Forwarding  https://3156-47-150-126-75.ngrok-free.app -> http://localhost:9044

Update .env.ngrok with the ngrok domain. In this case it would be 3156-47-150-126-75.ngrok-free.app

We launch the server using vscode launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "server-ngrok"
    }
  ]
}
  • require email verification for password and social logins
  • require email multifactor
  • totp auth app is not really needed. The reason is that a users email is more important than your app. They should be doing way more multi factor over there. i.e. github, google or microsoft social. Enterprise IDPS have required multifactor and it looks like social accounts are now requiring it as well. Doing it here is just redundant and leads to account recovery problems.
  • offer passkeys

Directories

Path Synopsis
cmd
go-client
This is an example application to demonstrate parsing an ID Token.
This is an example application to demonstrate parsing an ID Token.
server/docs
Package docs Code generated by swaggo/swag.
Package docs Code generated by swaggo/swag.
example
pkg
proto
external/user
Package user is a reverse proxy.
Package user is a reverse proxy.
helloworld
Package helloworld is a reverse proxy.
Package helloworld is a reverse proxy.
oidc/client
Package client is a reverse proxy.
Package client is a reverse proxy.
oidc/idp
Package idp is a reverse proxy.
Package idp is a reverse proxy.
oidc/user
Package user is a reverse proxy.
Package user is a reverse proxy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL