This guide covers everything you need to get up and running with Uitsmijter. The documentation is based on a fictional project to provide better understanding of when and why to set specific configurations.
This quick start guide assumes that the requirements are met. See this list of requirements that covers the following criteria:
- Kubernetes is up and running
- Traefik is up and running
- Your cluster is able to get valid certificates for ingresses, e.g. with cert-manager
To deploy a working instance of Uitsmijter, you need privileges on the Kubernetes cluster that allow you to deploy the following resource kinds:
A service account with a cluster role is needed to allow Uitsmijter to read its CustomResources
- ClusterRole
- ClusterRoleBinding
- ServiceAccount
CustomResources definitions are needed to declare Tenants and Clients:
- CustomResourceDefinition
Kubernetes resources will be installed during the installation:
- Namespace
- ConfigMap
- Secret
- Service
- Deployment
- StatefulSet
- Ingress
- HorizontalPodAutoscaler
The Interceptor-Mode relies on Traefik Middlewares that will be set up during the installation:
- Middleware
CustomResources declared by CustomResourceDefinition should allow your account to create, list, and edit in your namespaces:
- Client
- Tenant
Make sure that you have these rights on your cluster (an admin certainly will have all of these). If not, please ask your system administrator for help.
Uitsmijter offers a 🔗 Helm installation routine. Download the Values.yaml first and adjust the values for your needs. The following example describes the sections for a fictional project. You will need to change the values accordingly.
The Project Setup:
We are planning a new customer portal for the domain example.com. The portal should be available for customers to
send small notes to a selected group of recipients. However, we are planning to create different Microservices behind
a Single-Page-Application (SPA).
The SPA shows generally available content and offers a login button. Various functions are available only if a user is logged in. Without a valid login, the user sees marketing project information provided by a CMS. After login, the user has access to their own profile, address book, and incoming messages and is also allowed to write a new message to all participants in the address book.
The business requirements say that certain users with the partner role should have an extra functionality that is
available as a link to a portal that is made by another team. If the user is logged in to example.com then the user
should also be logged in to the other portal located at partner.example.com.
So far so good. The architecture of the new project is set and looks like this:
- portal.example.com (portal.example.com)
- partner.example.com (partner.example.com)
- CMS (cms.example.com)
- Profile backend (profile.srv.example.com)
- Address book backend (contacts.srv.example.com)
- Inbox backend (inbox.srv.example.com)
- Send messages backend (send.srv.example.com)
As you can see, we do make the services publicly available! We will secure them later with a JWT. To make them accessible from within the SPA, they should be publicly available; otherwise, we would need a 🔗 BFF.
Create a User Backend:
User data must be stored somewhere. Uitsmijter does not store any account data, profiles, or passwords. To create a store for user credentials, either a service must be created or selected from the existing ones. In our example, the
Profile backendwould fit, but we want to make this publicly available, and the user store should only be accessible within the cluster. We could create an extra route that is only available from a private service, but for the sake of security and the benefit of a new project, we will create a service dedicated to storing user credentials.
This new Credentials service got one route named: “POST: /validate-login” and fires a query against a database:
SELECT `id`, `role`, `profile`
FROM accounts
WHERE `username` = ?
AND `passwordhash` = ?;
In our example passwords are stored as a sha256-Hash. You can choose between sha256, md5 and plain text.
Other applications will populate the users after registration. This is out of scope for now. The important thing is that
/validate-login takes two parameters: username and passwordHash, and returns a status 200 with a user profile
object or an unauthorized error if the credentials do not match.
In case the credentials match, return the user profile object:
HTTP/1.1 200
Content-Type: application/json; charset=UTF-8
{
"id": "${result.id}",
"role": "${result.role}",
"profile": "${result.profile}",
}
We host this little service in the usertrunk namespace with a service that points to the deployment:
---
kind: Service
apiVersion: v1
metadata:
namespace: usertrunk
name: checkcredentials
spec:
selector:
app: userdb
ports:
- protocol: TCP
port: 80
targetPort: 8080
It’s time to install Uitsmijter!: At this point, we need a service that handles the authorization for our project. We do not want to log in multiple times to different portals, and we do not want to authenticate the user in all backends. Backends should deny access if a user requests with an invalid token, and access data on the user’s behalf if the token is correct.
That implies that we expect some criteria:
- The user must login to get a valid token
- The token must encode a unique
subjectto identify the user across all the backends - The SPA must retrieve the token securely
- To allow other Portals (like partner.example.com) to join the SSO, authorisation must be outside the main portal
Edit the Uitsmijter Values.yaml: In this section, we will go through all available settings and describe them in detail with recommended settings for the demo project described above.
namespaceOverride: ""
This value specifies the namespace in which Uitsmijter should be installed. We recommend installing into the default
namespace: uitsmijter. If you are planning to install into another namespace, you will need to adjust Middleware paths
later. This is straightforward if you know what you are doing, but can be confusing if you are new to Kubernetes or
🔗 Ingress middleware with Traefik. If you want to start without
hassle and debugging, it is highly recommended to install Uitsmijter in the default namespace first.
image:
repository: docker.ausdertechnik.de/uitsmijter/uitsmijter
# Overrides the image tag whose default is the chart appVersion.
tag: ""
If you downloaded the newest version from the public repository, the settings are fine and work out of the box.
Only if you host Docker images in a private repository do you need to change the image.repository path to point to your
private copy of the image. For example: docker.example.com/sso/uitsmijter.
We do not recommend hosting a single private copy of Uitsmijter in your own repository because we update the images to fix bugs and improve features frequently. To stay informed about updates and pull from the latest version, you may want to clone a mirror of the entire repository instead. If you do not know how to do this, please ask for assistance.
The version tag is set automatically according to the application version of the Helm chart. Please ensure that you
have downloaded the latest version.
Only if you are performing an upgrade do you need to set the version manually. For example, when upgrading from version 1.0.0 to
version 1.0.1, you need to set the tag:
tag: "1.0.1"
imagePullSecrets:
Default is blank because Uitsmijter is publicly available. However, if you are cloning the repository to your private one, it may be secured by an imagePullSecret. You can define the name of the secret here.
Beware that the secret must be present in the namespace of Uitsmijter!
Example:
imagePullSecrets:
- name: my-repository-pull-secret
jwtSecret: "vosai0za6iex8AelahGemaeBooph6pah6Saezae0oojahfa7Re6leibeeshiu8ie"
redisPassword: "Shohmaz1"
storageClassName: default-ext4
installCRD: true
installSA: true
You have to change the values of the passwords in jwtSecret and redisPassword!
The jwtSecret is a global passphrase with which all JWTs are signed. Applications dealing with JWTs must know
this shared secret. The jwtSecret should be set during installation and kept on the server only. We highly
recommend using 🔗 config-syncer to share the secret to other namespaces.
From the example above we decided that the Profile backend, Address book backend, Inbox backend
and Send messages backend will get their own namespaces to collect the backend and the databases, as well as services
and ingresses all together in the domain of the service:
- profile
- address
- inbox
- sender
The jwtSecret will be created as a secret in the uitsmijter namespace (if not changed with namespaceOverride).
All backends need to know the secret to validate incoming JWTs. Rather than creating manual
secrets in all four namespaces that can fall out of sync when rotating the secret (which you
should do from time to time), we recommend syncing the secret from the uitsmijter namespace to the profile, address, inbox,
and sender namespaces.
To sync the secret into namespaces add a label to the namespace the secret has to sync in:
---
apiVersion: v1
kind: Namespace
metadata:
name: profiles
labels:
jwt-secret/sync: "true"
jwt-secret/sync: "true" takes a look for the secret and syncs it into the namespace profiles. For more information
please take a look at
the 🔗 config-syncer documentation.
The Uitsmijter installation will set up a 🔗 Redis database to store refresh tokens.
The redisPassword will only be used inside the uitsmijter namespace, and you must replace the value during
installation.
Attention: after changing the Redis password, you must roll out Redis again and restart the services. We recommend generating a random password at the first installation and keeping it secret during implementation. To rotate the secret, you may want to return later and 🔗 read this article.
The storageClassName highly depends on your Kubernetes installation.
You can list all available storage classes with kubectl:
kubectl get sc
Make sure that you choose a storage class that is available on all of your nodes. For more information read the documentation that 🔗 describes the concept of a StorageClass in Kubernetes.
config:
# Log format options: console|ndjson
logFormat: "console"
# Log level options: trace|info|error|critical
logLevel: "info"
cookieExpirationInDays: 7
tokenExpirationInHours: 2
tokenRefreshExpirationInHours: 720
logFormat:
The log format can be switched between console and ndjson. Console will print each log entry on a single line
with the level and server time:
[NOTICE] Wed, 21 Dec 2022 10:48:24 GMT: Server starting on http://127.0.0.1:8080
If you are using a log aggregator, it is more convenient to log in 🔗 ndjson:
{"function":"start(address:)","level":"NOTICE","date":"2022-12-21T10:52:18Z","message":"Server starting on http:\/\/127.0.0.1:8080"}
logLevel:
The standard log level is info and provides a good overview of what Uitsmijter is doing. info also prints
notices, errors, and critical alerts.
If you want to see more of the application’s behavior, you may want to enable the development trace logs. If
you only want alerts about issues, you can suppress most info and notices by setting
the log level to error.
Everything about logging is described in this separate section
cookieExpirationInDays:
You can adjust how many days a cookie is valid without refreshing its value. A valid cookie means the user is logged in.
This is highly important for Interceptor-Mode because if you delete a user, they can still use your service for
the duration of the cookie lifetime! A good starting value is 1 day. A deleted user remains valid for a maximum of 24 hours
in Interceptor-Mode and for a maximum of tokenExpirationInHours for
each OAuth-Flow.
The cookie expiration time has to be always equal or greater than the token expiration. In the example project we assume that a user pays in a monthly subscription, and we do not have external resources protected with interceptor yet. In this case 7 days is a very good starting point while development the services and will fit our needs later on, too.
tokenExpirationInHours: In OAuth-Flow, the user exchanges an authorization code (see grant_types) for an access and refresh token. If the access token expires, a new valid one can be obtained with the refresh token.
As long as the access token has not expired, a user is logged in, even if the user has been deleted from the credentials
service.
With the example value of 2 hours, the user can access our portal for a maximum of 2 hours before being kicked out.
This setting is independent of the cookie lifetime.
Special case silent login: If silent login is turned on, the login might happen automatically! You should only rely on the token expiration time when silent login is turned off (enabled by default). More information is provided in the tenant and client configuration section.
tokenRefreshExpirationInHours: For every code exchange and every refresh, the authorization server generates a pair of an access token and a refresh token. The access token is a Bearer-encoded JWT with the user profile encoded. The refresh token is a random key that can be used to refresh the access token. If an access token becomes invalid, the user (or the library being used) can obtain a new valid access token with the refresh token (see grant_types).
Uitsmijter stores refresh tokens for a defined amount of time. If a user has a valid and known refresh token, an access token can be requested.
Therefore, the refresh expiration period must be longer than the access token expiration.
Do you know those mobile Apps where you are always logged in after initial registration? Those apps know you because they have a very long refresh token period (sometimes ~1 year). When opening the app the first thing is to exchange the access token, regardless of the period, with the very long-lived refresh token. This is the way you are always signed in. In our example after 30 days (720 hours) of inactivity the user must log in with credentials again.
Our recommendation for the first installation is to use the default settings. You may want to adjust the settings later to fit your business model. If you need any assistance, please do not hesitate to contact our consultants or ask the community.
Uitsmijter should run on at least one domain. We say “at least” because Uitsmijter is multi-tenant and multi-client aware, and one instance can run on more than one domain. For large installations with multiple different brands, it may be a good idea to run one clustered Uitsmijter instance and provide login functionality to different domains so that a login does not change the main domain, ensuring the trust level for your customers.
domains:
- domain: "login.ham.test"
tlsSecretName: "ham.test"
- domain: "id.example.com"
tlsSecretName: "example.com"
In the example above, Uitsmijter is available at login.ham.test and also at id.example.com. Both domains
point to the same instance.
For both of our example portals we just need one domain:
domains:
- domain: "id.example.com"
tlsSecretName: "example.com"
Uitsmijter supports 🔗 Horizontal Pod Autoscaling well. For more details please take a look at hpa.yaml inside the helm templates.
You can set the minimum and maximum amount of replicas in the hpa.yaml. The default is set to minReplicas: 1
and maxReplicas: 3.
Congratulations, the hard part is done. You have configured your Uitsmijter installation successfully. Most of the values should be the same as given in defaults, that is ok, you can revisit and fine tune the server later on.
To install Uitsmijter onto your cluster a Helm Chart is provided. If you have access to the cluster and check the privileges mentioned above, the following steps install everything right in place.
helm repo add uitsmijter https://charts.uitsmijter.io/
helm update
helm install uitsmijter uitsmijter/uitsmijter
Read more about the helm charts configuration.
After installation make sure that your user has the rights to edit
ClientsandTenantsat least in your namespaces.
In the project example, we are setting up Uitsmijter for one domain and one company. Only one tenant is needed. Examples
for a multi-tenant setup are given in the tenant and client configuration section.
Our single tenant is called portal. For the configuration of this tenant, we first create a new namespace
to collect all overall settings:
kubectl create ns portal
In that namespace, we will add the tenant. First, we need to define it:
---
apiVersion: "uitsmijter.io/v1"
kind: Tenant
metadata:
name: portal
spec:
hosts:
- portal.example.com
- partner.example.com
interceptor:
enabled: false
domain: login.example.com
cookie: .example.com
providers:
- |
class UserLoginProvider {
constructor(credentials) { commit(true); }
get canLogin() { return true; }
get userProfile() { return {message:"DO NOT USE THIS IN PRODUCTION"}; }
get role() { return "development"; }
}
- |
class UserValidationProvider {
constructor(args) { commit(true); }
isValid() { return true; }
}
Save the file to portal-tenant.yaml.
Important for HPA: Change the ident with a new generated uuid and keep it consistent along the tenant name.
You can learn everything about tenants in tenant and client configuration
section.
To get started quickly we have to care about the providers only. The script above is just a working example that
logs in every user with every password. That is not what we want. We have created a credentials service above
that checks the user credentials in a database and returns a profile if found. The service takes an username and
a hashed passwordinput.
You can learn everything about Providers on the General provider information page and explicit about the UserLoginProvider on the User Login Provider page.
- The described
Credentials serviceprovides a route “POST: /validate-login” and is accessible within the cluster only. - We host the service
checkcredentialsin the namespaceusertrunk. It is internally available at:checkcredentials.usertrunk.svc.cluster.local. - The service expects a sha265 hashed password, because we do not send cleartext passwords to other services!
The provider scripts should look like this:
class UserLoginProvider {
isLoggedIn = false;
profile = {};
role = null;
constructor(credentials) {
fetch(`http://checkcredentials.usertrunk.svc.cluster.local/validate-login`, {
method: "post",
body: {
username: credentials.username,
passwordHash: sha256(credentials.password)
}
}).then((result) => {
var subject = {};
profile = JSON.parse(result.body);
if (result.code == 200) {
this.isLoggedIn = true;
this.role = profile.role;
subject = {subject: profile.userId};
}
commit(result.code, subject);
}
);
}
get canLogin() {
return this.isLoggedIn;
}
get userProfile() {
return this.profile;
}
get role() {
return this.role;
}
}
class UserValidationProvider {
isValid = false;
constructor(args) {
fetch(`http://checkcredentials.usertrunk.svc.cluster.local/validate-user`, {
method: "post",
body: {
username: args.username,
}
}).then((result) => {
var subject = {};
profile = JSON.parse(result.body);
if (result.code == 200) {
this.isValid = true;
}
commit(this.isValid);
}
);
}
get isValid() {
return this.isValid;
}
}
Update the script in ll-tenant.yaml.
The script will send the users username and a sha265 hashed password to
checkcredentials.usertrunk.svc.cluster.local/validate-login and if this endpoint responses successfully the
script prepares the class variables that are consumed by the auth server later in the auth process.
That’s it. Your first secure tenant is set up and connected to your users service.
Apply the tenant to the namespace we have created above:
kubectl apply -n portal portal-tenant.yaml
To connect an OAuth client with Uitsmijter we also have to define a client. A tenant can have multiple clients (e.g. for an SPA and an App). The client defines what OAuth-Flows are allowed and what scopes a user can have if asked for.
Here is an example client for our SPA at portal.portal.com:
---
apiVersion: "uitsmijter.io/v1"
kind: Client
metadata:
name: example-portal
spec:
ident: 540FF520-2BDF-4C6F-9D9F-DC88A9DB41F6
tenantname: portal/portal
redirect_urls:
- https://portal.example.com/.*
grant_types:
- authorization_code
- refresh_token
scopes:
- access
- profile::read
- profile::write
- addressbook:read
- addressbook:write
- addressbook:delete
- inbox:read
- inbox:delete
- sendmessages
referrers:
- https://.*.example.com/.*
isPkceOnly: true
The tenant name must match and the uuid ident must be unique in the Uitsmijter universe on your cluster.
We only allow clients that are connected from the referrers: https://..example.com/., that includes all
subdomains of example.com and login can happen from any path of that domains.
This makes it possible to request a login even from a landing page like specialoffers.example.com, but the
redirect is allowed to https://portal.example.com/.* only. So after login the user will be redirected to our
portal.
This does not work for the partner portal. Either we create a new client for that, or we expand the redirect_urls
array
redirect_urls:
- https://portal.example.com/.*
- https://partner.example.com/.*
But because the partner portal is made by another team, we recommend to add a second client. To learn all about the client settings please take a look at the tenant and client configuration section.
Uitsmijter automatically reloads changed tenants and clients. Take a look at the logs to see if the tenant and client is loaded without errors:
kubectl logs -n uitsmijter -l app=uitsmijter -l component=authserver
You should see something similar to these lines:
Fount 1 items in TenantList/uitsmijter.io/v1
Found tenant in crd: example-portal from namespace: portal
Load tenant from crd: example-portal successfully
Added new tenant 'example-portal' [EDB1B825-CFED-41F0-A844-682D7B695B72] with 1 hosts
and the client
Fount 5 items in ClientList/uitsmijter.io/v1
Found client in crd: example-portal from namespace: portal
Load client from crd: example-portal
Added new client 'example-portal' [540FF520-2BDF-4C6F-9D9F-DC88A9DB41F6] for tenant 'portal'
Congratulations!! All is set up, and you can build your portal with an OAuth login flow. If you aren’t familiar with Single page OAuth flows, we have prepared a little demo application at spa.littleletter.de. The sourcecode is available. Please ask for any assistance.