Project: Pinniped (Provisioning / Key Management)
File: ACCEPTANCE-NOTES.md

Rather than writing a webapp to manually test the dynamic client features during user story acceptance, we can simulate the requests that a webapp would make to the Supervisor using the commands shown below. The commands below the happy path for a fully-capable OIDCClient which is allowed to use all supported grant types and scopes. These commands can be adjusted to test other scenarios of interest. We can use the developer hack scripts to deploy a working Supervisor on a local Kind cluster. These clusters have no ingress, so we use Kind's port mapping feature to expose a web proxy outside the cluster. The proxy can then be used to access the Supervisor. In this setup, the Supervisor's CA is not trusted by the web browser, however, the curl commands can trust the CA cert by using the `--cacert` flag. ```shell ./hack/prepare-for-integration-tests.sh -c source /tmp/integration-test-env ./hack/prepare-supervisor-on-kind.sh --ldap --flow browser_authcode ``` Alternatively, the Supervisor could be installed into a cluster in a more production-like way, with ingress, a DNS entry, and TLS certs. In this case, the proxy env vars used below would not be needed, and the issuer string would be adjusted to match the Supervisor's ingress DNS hostname. ```shell cat <<EOF | kubectl apply -f - apiVersion: config.supervisor.pinniped.dev/v1alpha1 kind: OIDCClient metadata: name: client.oauth.pinniped.dev-my-webapp-client namespace: supervisor # must be in the same namespace as the Supervisor spec: allowedRedirectURIs: https://webapp.example.com/callback allowedGrantTypes: authorization_code refresh_token urn:ietf:params:oauth:grant-type:token-exchange allowedScopes: openid offline_access pinniped:request-audience username groups EOF ``` Get the OIDCClient to check its status: ```shell kubectl get oidcclient -n supervisor client.oauth.pinniped.dev-my-webapp-client -o yaml ``` Create a client secret for the OIDCClient: ```shell cat <<EOF | kubectl create -o yaml -f - apiVersion: clientsecret.supervisor.pinniped.dev/v1alpha1 kind: OIDCClientSecretRequest metadata: name: client.oauth.pinniped.dev-my-webapp-client # the name of the OIDCClient namespace: supervisor # the namespace of the OIDCClient spec: generateNewSecret: true EOF ``` Example response: ```yaml apiVersion: clientsecret.supervisor.pinniped.dev/v1alpha1 kind: OIDCClientSecretRequest metadata: creationTimestamp: null spec: generateNewSecret: false revokeOldSecrets: false status: generatedSecret: 0cc65d46fb5c0fb80123b28bd8093ae0e61e568b6c35cbca82941dcaa8c67b5b totalClientSecrets: 1 ``` Make a note of the `generatedSecret` value. It will never be shown again. The OIDC authcode flow always starts with an authorization request. A webapp would redirect the user's browser to make this request in a browser. For story acceptance, this request could also be made in a web browser by typing the full URL with params into the browser's address bar, although here we'll show how to use curl to ensure that we are documenting the exact requirements of the authorization request. Authorization parameter notes: Authorization requests must use PKCE. For manual testing, these sample values can be

For production use, each authorization request must have a new PKCE value computed for that request. Example code challenge: vTu6b5Jm2hpi1vjRJw7HB820EYNq7AFT1IHDLBQMc3Q Example code verifier: UDABWPiROQh0nfhGzd7OetrEJZZ7S-ZH8_ZLB2i8Yc2wix Nonce values should also be unique per authorization request in production use. State values are optional and will be passed back in the authcode callback if provided. ```shell PARAMS='?response_type=code'\ '&client_id=client.oauth.pinniped.dev-my-webapp-client'\ '&code_challenge=vTu6b5Jm2hpi1vjRJw7HB820EYNq7AFT1IHDLBQMc3Q'\ '&codechallengemethod=S256'\ '&nonce=9902045656a1c29b95515f7f45b40773'\ '&redirect_uri=https%3A%2F%2Fwebapp.example.com%2Fcallback'\ '&scope=openid+offline_access+username+groups+pinniped%3Arequest-audience'\ '&state=cfcd3a3e72774bee1e748e6bf4a70f5c' httpsproxy="http://127.0.0.1:12346" noproxy="127.0.0.1" \ curl -vfLsS --cookie-jar cookies.txt --cacert root_ca.crt \ "https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/oauth2/authorize$PARAMS" ``` When successful, this should redirect to the Supervisor's LDAP login page and return its HTML. The resulting HTML form will include a hidden param called `state`. Make a note of its value for the next step. The LDAP login page's form can be submitted with: ```shell STATE='COPYPASTEHIDDENSTATEPARAMFROMPREVIOUSCURLRESULT_HERE' httpsproxy="http://127.0.0.1:12346" noproxy="127.0.0.1" \ curl -vfsS --cookie cookies.txt --cacert root_ca.crt \ "https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/login" \ --form-string "username=$PINNIPEDTESTLDAPUSERCN" \ --form-string "password=$PINNIPEDTESTLDAPUSERPASSWORD" \ --form-string "state=$STATE" ``` When successful, this should result in an HTTP 302 or 303 redirect response. The `location` header should look something like `https://webapp.example.com/callback?code=pinacoq7m9z...wuzQ&scope=openid+offline_access+pinniped%3Arequest-audience+username+groups&state=cfcd3a3e72774bee1e748e6bf4a70f5c` which includes the authcode as the `code` param. Make a note of its value for the next step. The authcode callback would be handled by the webapp's backend. The backend code would then use the authcode to make a token request to the Supervisor. This would happen as a backend request, so the user's browser would not be involved. ```shell CODE='COPYAUTHCODEFROMPREVIOUSCURLRESULTHERE' CLIENTSECRET='COPYCLIENTSECRETHERE' httpsproxy="http://127.0.0.1:12346" noproxy="127.0.0.1" \ curl -vfsS --cacert root_ca.crt \ "https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/oauth2/token" \ --form-string "granttype=authorizationcode" \ --form-string "code=$CODE" \ --form-string "redirect_uri=https://webapp.example.com/callback" \ --form-string "codeverifier=UDABWPiROQh0nfhGzd7OetrEJZZ7S-ZH8ZLB2i8Yc2wix" \ -u "client.oauth.pinniped.dev-my-webapp-client:$CLIENT_SECRET" ``` When successful, this should return some JSON which includes the Supervisor-issued tokens. The ID token can be decoded for inspection (e.g. using https://jwt.io). Make a note of the access token and the refresh token for the next steps. If the webapp wanted to access a Kubernetes cluster on behalf of the end user, it would need to make an additional request (per cluster) to get a cluster-scoped ID token. ```shell ACCESS='COPYACCESSTOKENFROMPREVIOUSCURLRESULT_HERE' httpsproxy="http://127.0.0.1:12346" noproxy="127.0.0.1" \ curl -vfsS --cacert root_ca.crt \ "https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/oauth2/token" \ --form-string "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ --form-string "audience=my-workload-cluster-audience-name" \ --form-string "subject_token=$ACCESS" \ --form-string "subjecttokentype=urn:ietf:params:oauth:token-type:access_token" \ --form-string "requestedtokentype=urn:ietf:params:oauth:token-type:jwt" \ -u "client.oauth.pinniped.dev-my-webapp-client:$CLIENT_SECRET" ``` If successful, this should return some JSON with a new cluster-scoped ID token in the response. The ID and access tokens are very short-lived, so the backend of the webapp should refresh them as needed. ```shell REFRESH='COPYREFRESHTOKENFROMPREVIOUSCURLRESULT_HERE' httpsproxy="http://127.0.0.1:12346" noproxy="127.0.0.1" \ curl -vfsS --cacert root_ca.crt \ "https://pinniped-supervisor-clusterip.supervisor.svc.cluster.local/some/path/oauth2/token" \ --form-string "granttype=refreshtoken" \ --form-string "refresh_token=$REFRESH" \ -u "client.oauth.pinniped.dev-my-webapp-client:$CLIENT_SECRET" ``` When successful, this should return some JSON which includes the new Supervisor-issued tokens. The old refresh token is revoked and the next refresh request must use the newest refresh token.