450 lines
15 KiB
Markdown
450 lines
15 KiB
Markdown
# Werther <sup>[1](#myfootnote1)</sup>
|
|
|
|
[![GoDoc][doc-img]][doc] [![Build Status][build-img]][build] [![codecov][codecov-img]][codecov] [![Go Report Card][goreport-img]][goreport]
|
|
|
|
Werther is an Identity Provider for [ORY Hydra][hydra] over [LDAP][ldap].
|
|
It implements [Login And Consent Flow][hydra-login-consent] and provides basic UI.
|
|
|
|
![screenshot](.github/media/screenshot.gif)
|
|
|
|
**Features**
|
|
- Support [Active Directory][ad];
|
|
- Mapping LDAP attributes to OpenID Connect claims;
|
|
- Mapping LDAP groups to user roles;
|
|
- OAuth 2.0 scopes;
|
|
- Caching users roles;
|
|
- UI customization.
|
|
|
|
**Limitations**
|
|
- Werther grants all requested permissions to a client without displaying the consent page;
|
|
- Werther confirms a logout request without displaying the logout confirmation page.
|
|
|
|
**Requirements**
|
|
|
|
ORY Hydra v1.0.0-rc.12 or higher.
|
|
|
|
**Table of Contents**
|
|
<!-- To generate the table use the command "npx doctoc --maxlevel 2 README.md" -->
|
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
|
|
|
|
- [Installing](#installing)
|
|
- [Configuration](#configuration)
|
|
- [User roles](#user-roles)
|
|
- [UI customization](#ui-customization)
|
|
- [Example](#example)
|
|
- [Resources](#resources)
|
|
- [Footnotes](#footnotes)
|
|
- [Contributing](#contributing)
|
|
- [License](#license)
|
|
|
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
|
|
## Installing
|
|
|
|
### From Docker
|
|
|
|
```bash
|
|
docker pull icoreru/werther
|
|
```
|
|
|
|
### From sources
|
|
|
|
```bash
|
|
go install ./...
|
|
```
|
|
|
|
## Configuration
|
|
|
|
The application is configured via environment variables.
|
|
Names of the environment variables starts with prefix `WERTHER_`.
|
|
See a list of the environment variables using the command:
|
|
|
|
```
|
|
werther -h
|
|
```
|
|
|
|
## User roles
|
|
|
|
In LDAP user's roles are groups in which a user is a member.
|
|
|
|
The environment variable `WERTHER_LDAP_ROLE_DN` is a DN for searching roles.
|
|
|
|
For example, create an OU that repserents an application, and then in the created OU
|
|
create groups that represent application's roles:
|
|
|
|
```
|
|
dc=com
|
|
|-- dc=example
|
|
|-- ou=AppRoles
|
|
|-- ou=App1
|
|
|-- cn=app1_role1 (objectClass="group", description="role1")
|
|
|-- cn=app1_role2 (objectClass="group", description="role2")
|
|
```
|
|
|
|
Run Werther with the environment variable `WERTHER_LDAP_ROLE_DN`
|
|
that equals to `ou=AppRoles,dc=example,dc=com`.
|
|
|
|
In the above example Werther returns user's roles as a value
|
|
of the user role's claim `https://github.com/i-core/werther/claims/roles`.
|
|
|
|
```json
|
|
{
|
|
"https://github.com/i-core/werther/claims/roles": {
|
|
"App1": ["role1", "role2"],
|
|
}
|
|
}
|
|
```
|
|
|
|
To customize the roles claim's name you should set a value of the environment variable `WERTHER_LDAP_ROLE_CLAIM`.
|
|
Also you should map the custom name of the roles' claim to a roles's scope using the environment variable
|
|
`WERTHER_IDENTP_CLAIM_SCOPES` (the name must be [URL encoded][uri-spec-encoding]):
|
|
|
|
```bash
|
|
env WERTHER_LDAP_ROLE_CLAIM=https://my-company.com/claims/roles \
|
|
WERTHER_IDENTP_CLAIM_SCOPES=name:profile,family_name:profile,given_name:profile,email:email,https%3A%2F%2Fmy-company.com%2Fclaims%2Froles:roles \
|
|
werther
|
|
```
|
|
|
|
For more details about claims naming see [OpenID Connect Core 1.0][oidc-spec-additional-claims].
|
|
|
|
**NB** There are cases when we need to create several roles with the same name in LDAP.
|
|
For example, when we want to configure multiple applications or several environments for the same application.
|
|
|
|
```
|
|
dc=com
|
|
|-- dc=example
|
|
|-- ou=AppRoles
|
|
|-- ou=Test
|
|
|-- ou=App1
|
|
|-- cn=test_app1_role1 (objectClass="group", description="role1")
|
|
|-- cn=test_app1_role2 (objectClass="group", description="role2")
|
|
|-- ou=App2
|
|
|-- cn=test_app2_role1 (objectClass="group",description-"role1")
|
|
|-- cn=test_app2_role2 (objectClass="group",description-"role2")
|
|
|-- ou=Dev
|
|
|-- ou=App1
|
|
|-- cn=dev_app1_role1 (objectClass="group", description="role1")
|
|
|-- cn=dev_app1_role3 (objectClass="group", description="role3")
|
|
|-- ou=App2
|
|
|-- cn=dev_app2_role1 (objectClass="group",description-"role1")
|
|
|-- cn=dev_app2_role4 (objectClass="group",description-"role4")
|
|
```
|
|
|
|
Active Directory requires unique CNs in a domain. But in Active Directory
|
|
creating groups with the same CN in different OUs is difficult.
|
|
Because of it, Werther uses a LDAP attribute as a role's name instead of CN.
|
|
A name of a LDAP attribute is specified using the environment variable `WERTHER_LDAP_ROLE_ATTR`,
|
|
and has the default value `description`.
|
|
|
|
In the above example, Werther returns a response that contains the next roles:
|
|
* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `ou=Test,ou=AppRoles,dc=example,dc=com`:
|
|
```json
|
|
{
|
|
"https://github.com/i-core/werther/claims/roles": {
|
|
"App1": ["role1", "role2"],
|
|
"App2": ["role1", "role2"]
|
|
}
|
|
}
|
|
```
|
|
* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `ou=Dev,ou=AppRoles,dc=example,dc=com`:
|
|
```json
|
|
{
|
|
"https://github.com/i-core/werther/claims/roles": {
|
|
"App1": ["role1", "role3"],
|
|
"App2": ["role1", "role4"]
|
|
}
|
|
}
|
|
```
|
|
|
|
## UI customization
|
|
|
|
Werther uses the Go templates to render UI pages.
|
|
To customize the UI you should create a directory that contains UI pages' templates.
|
|
After that you should set the directory path to the environment variable `WERTHER_WEB_DIR`.
|
|
|
|
### Custom login page
|
|
|
|
A login page's template must be a Go template. The template has access to data conforming the next JSON-schema:
|
|
|
|
```yaml
|
|
type: object
|
|
properties:
|
|
- WebBasePath:
|
|
description: The base path of the login page
|
|
type: string
|
|
- LangPrefs:
|
|
description: The user language preferences (the parsed value of the header Accept-Language)
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
- Lang:
|
|
description: The language canonical name.
|
|
type: string
|
|
- Weight:
|
|
description: The language weight.
|
|
type: number
|
|
required:
|
|
- Lang
|
|
- Weight
|
|
- Data:
|
|
type: object
|
|
properties:
|
|
- CSRFToken:
|
|
description: A CSRF token.
|
|
type: string
|
|
- Challenge:
|
|
description: A login challenge ID.
|
|
type: string
|
|
- LoginURL:
|
|
description: An endpoint that finishes the login process.
|
|
type: string
|
|
- IsInvalidCredentials:
|
|
description: Specifies that a user types an invalid username or password.
|
|
type: boolean
|
|
- IsInternalError:
|
|
description: Specifies that an internal server error happens when finishing the login process.
|
|
type: boolean
|
|
required:
|
|
- CSRFToken
|
|
- Challenge
|
|
- LoginURL
|
|
- IsInvalidCredentials
|
|
- IsInternalError
|
|
required:
|
|
- WebBasePath
|
|
- LangPrefs
|
|
- Data
|
|
```
|
|
|
|
When a login page's template contains static resources (like styles, scripts, and images)
|
|
they must be placed in a subdirectory called `static`.
|
|
|
|
For a full example of a login page's template see [source code](internal/web/templates).
|
|
|
|
### Custom login page (old format)
|
|
|
|
*The old template format is also supported but it will be removed in the future major release.*
|
|
|
|
A login page's template should contains blocks `title`, `style`, `script`, `content`.
|
|
Each block has access to data conforming the next JSON-schema:
|
|
|
|
```yaml
|
|
type: object
|
|
properties:
|
|
- CSRFToken:
|
|
description: A CSRF token.
|
|
type: string
|
|
- Challenge:
|
|
description: A login challenge ID.
|
|
type: string
|
|
- LoginURL:
|
|
description: An endpoint that finishes the login process.
|
|
type: string
|
|
- IsInvalidCredentials:
|
|
description: Specifies that a user types an invalid username or password.
|
|
type: boolean
|
|
- IsInternalError:
|
|
description: Specifies that an internal server error happens when finishing the login process.
|
|
type: boolean
|
|
required:
|
|
- CSRFToken
|
|
- Challenge
|
|
- LoginURL
|
|
- IsInvalidCredentials
|
|
- IsInternalError
|
|
```
|
|
|
|
When a login page's template contains static resources (like styles, scripts, and images)
|
|
they must be placed in a subdirectory called `static`.
|
|
|
|
For a full example of a login page's template see [source code](internal/web/templates).
|
|
|
|
## Example
|
|
|
|
1. Create file `ldap.ldif`:
|
|
```
|
|
dn: uid=kolya_gerasyimov,ou=Users,dc=example,dc=com
|
|
objectClass: inetOrgPerson
|
|
cn: Kolya Gerasyimov
|
|
sn: Gerasyimov
|
|
uid: kolya_gerasyimov
|
|
userPassword: 123
|
|
mail: kolya_gerasyimov@example.com
|
|
ou: Users
|
|
|
|
dn: ou=AppRoles,dc=example,dc=com
|
|
objectClass: organizationalunit
|
|
ou: AppRoles
|
|
description: AppRoles
|
|
|
|
dn: ou=App1,ou=AppRoles,dc=example,dc=com
|
|
objectClass: organizationalunit
|
|
ou: App1
|
|
description: App1
|
|
|
|
dn: cn=traveler,ou=App1,ou=AppRoles,dc=example,dc=com
|
|
objectClass: groupofnames
|
|
cn: traveler
|
|
description: traveler
|
|
member: uid=kolya_gerasyimov,ou=Users,dc=example,dc=com
|
|
```
|
|
|
|
2. Create file `docker-compose.yml`:
|
|
```yaml
|
|
version: "3"
|
|
services:
|
|
hydra-client:
|
|
image: oryd/hydra:v1.0.0-rc.12
|
|
environment:
|
|
HYDRA_ADMIN_URL: http://hydra:4445
|
|
command:
|
|
- clients
|
|
- create
|
|
- --skip-tls-verify
|
|
- --id
|
|
- test-client
|
|
- --secret
|
|
- test-secret
|
|
- --response-types
|
|
- id_token,token,"id_token token"
|
|
- --grant-types
|
|
- implicit
|
|
- --scope
|
|
- openid,profile,email,roles
|
|
- --callbacks
|
|
- http://localhost:3000
|
|
- --post-logout-callbacks
|
|
- http://localhost:3000/post-logout-callback
|
|
networks:
|
|
- hydra-net
|
|
deploy:
|
|
restart_policy:
|
|
condition: none
|
|
depends_on:
|
|
- hydra
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://hydra:4445"]
|
|
interval: 10s
|
|
timeout: 10s
|
|
retries: 10
|
|
hydra:
|
|
image: oryd/hydra:v1.0.0-rc.12
|
|
environment:
|
|
URLS_SELF_ISSUER: http://localhost:4444
|
|
URLS_SELF_PUBLIC: http://localhost:4444
|
|
URLS_LOGIN: http://localhost:8080/auth/login
|
|
URLS_CONSENT: http://localhost:8080/auth/consent
|
|
URLS_LOGOUT: http://localhost:8080/auth/logout
|
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone,roles
|
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number,https://github.com/i-core/werther/claims/roles
|
|
DSN: memory
|
|
command: serve all --dangerous-force-http
|
|
networks:
|
|
- hydra-net
|
|
ports:
|
|
- "4444:4444"
|
|
- "4445:4445"
|
|
deploy:
|
|
restart_policy:
|
|
condition: on-failure
|
|
depends_on:
|
|
- werther
|
|
werther:
|
|
image: icoreru/werther:v1.1.1
|
|
environment:
|
|
WERTHER_IDENTP_HYDRA_URL: http://hydra:4445
|
|
WERTHER_LDAP_ENDPOINTS: ldap:389
|
|
WERTHER_LDAP_BINDDN: cn=admin,dc=example,dc=com
|
|
WERTHER_LDAP_BINDPW: password
|
|
WERTHER_LDAP_BASEDN: "dc=example,dc=com"
|
|
WERTHER_LDAP_ROLE_BASEDN: "ou=AppRoles,dc=example,dc=com"
|
|
networks:
|
|
- hydra-net
|
|
ports:
|
|
- "8080:8080"
|
|
deploy:
|
|
restart_policy:
|
|
condition: on-failure
|
|
depends_on:
|
|
- ldap
|
|
ldap:
|
|
image: pgarrett/ldap-alpine
|
|
volumes:
|
|
- "./ldap.ldif:/ldif/ldap.ldif"
|
|
networks:
|
|
- hydra-net
|
|
ports:
|
|
- "389:389"
|
|
deploy:
|
|
restart_policy:
|
|
condition: on-failure
|
|
networks:
|
|
hydra-net:
|
|
```
|
|
|
|
3. Run the command:
|
|
```bash
|
|
docker stack deploy -c docker-compose.yml auth
|
|
```
|
|
|
|
4. Open the browser with http://localhost:4444/oauth2/auth?client_id=test-client&response_type=token&scope=openid%20profile%20email%20roles&state=12345678.
|
|
|
|
## Resources
|
|
|
|
- [Introduction to ORY Hydra, OAuth 2.0, and OpenID Connect][hydra-doc];
|
|
- [ORY Hydra: Integrating with (existing) User Management][hydra-login-consent];
|
|
- [ORY Hydra: Configuration][hydra-doc-config];
|
|
- [ORY Hydra: Official User Login & Consent Example][hydra-login-consent-example];
|
|
- [OpenID Connect Core 1.0][oidc-spec-core];
|
|
- [OpenID Connect Session Management 1.0][oidc-spec-session];
|
|
- [OpenID Connect Front-Channel Logout 1.0][oidc-spec-front-channel-logout];
|
|
- [OpenID Connect Back-Channel Logout 1.0][oidc-spec-back-channel-logout].
|
|
|
|
## Footnotes
|
|
|
|
1. <a name="myfootnote1"></a> Werther is named after robot Werther from [Guest from the Future](https://en.wikipedia.org/wiki/Guest_from_the_Future).
|
|
|
|
## Contributing
|
|
|
|
Thanks for your interest in contributing to this project.
|
|
Get started with our [Contributing Guide][contrib].
|
|
|
|
## License
|
|
|
|
The code in this project is licensed under [MIT license][license].
|
|
|
|
[doc-img]: https://godoc.org/github.com/i-core/werther?status.svg
|
|
[doc]: https://godoc.org/github.com/i-core/werther
|
|
|
|
[build-img]: https://travis-ci.com/i-core/werther.svg?branch=master
|
|
[build]: https://travis-ci.com/i-core/werther
|
|
|
|
[codecov-img]: https://codecov.io/gh/i-core/werther/branch/master/graph/badge.svg
|
|
[codecov]: https://codecov.io/gh/i-core/werther
|
|
|
|
[goreport-img]: https://goreportcard.com/badge/github.com/i-core/werther
|
|
[goreport]: https://goreportcard.com/report/github.com/i-core/werther
|
|
|
|
[contrib]: https://github.com/i-core/.github/blob/master/CONTRIBUTING.md
|
|
[license]: LICENSE
|
|
|
|
[ldap]: https://ldap.com/
|
|
[ad]: https://docs.microsoft.com/ru-ru/windows/desktop/AD/active-directory-domain-services
|
|
|
|
[hydra]: https://www.ory.sh/
|
|
[hydra-doc]: https://www.ory.sh/docs/hydra/
|
|
[hydra-login-consent]: https://www.ory.sh/docs/hydra/oauth2
|
|
[hydra-login-consent-example]: https://github.com/ory/hydra-login-consent-node
|
|
[hydra-doc-config]: https://www.ory.sh/docs/hydra/configuration
|
|
|
|
[oidc-spec-core]: https://openid.net/specs/openid-connect-core-1_0.html
|
|
[oidc-spec-additional-claims]: https://openid.net/specs/openid-connect-core-1_0.html#AdditionalClaims
|
|
[oidc-spec-session]: https://openid.net/specs/openid-connect-session-1_0.html
|
|
[oidc-spec-front-channel-logout]: https://openid.net/specs/openid-connect-frontchannel-1_0.html
|
|
[oidc-spec-back-channel-logout]: https://openid.net/specs/openid-connect-backchannel-1_0.html
|
|
|
|
[uri-spec-encoding]: https://tools.ietf.org/html/rfc3986#section-2
|