We are using Docker, Oathkeeper, Kratos, and Angular as a SPA.
kratos.json
{
"selfservice": {
"strategies": {
"password": {
"enabled": true
}
},
"settings": {
"privileged_session_max_age": "1m",
"after": {
"profile": {
"hooks": [
{
"hook": "verify"
}
]
}
}
},
"verify": {
"return_to": "https://myapp.com/"
},
"logout": {
"redirect_to": "https://myapp.com/"
},
"login": {
"request_lifespan": "10m"
},
"registration": {
"request_lifespan": "10m",
"after": {
"password": {
"hooks": [
{
"hook": "session"
},
{
"hook": "verify"
}
]
}
}
}
},
"log": {
"level": "debug"
},
"secrets": {
"session": [
"PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"
]
},
"urls": {
"login_ui": "https://myapp.com/auth/login",
"registration_ui": "https://myapp.com/auth/registration",
"error_ui": "https://myapp.com/error",
"settings_ui": "https://myapp.com/settings",
"verify_ui": "https://myapp.com/verify",
"mfa_ui": "https://myapp.com/",
"self": {
"public": "https://myapp.com/.ory/kratos/public/",
"admin": "http://kratos:4434/"
},
"default_return_to": "https://myapp.com/",
"whitelisted_return_to_urls": [
"https://myapp.com"
]
},
"hashers": {
"argon2": {
"parallelism": 1,
"memory": 131072,
"iterations": 2,
"salt_length": 16,
"key_length": 16
}
},
"identity": {
"traits": {
"default_schema_url": "file:///etc/config/kratos/identity.traits.schema.json"
}
},
"courier": {
"smtp": {
"connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true"
}
}
}
oathkeeper.config
serve:
proxy:
port: 4455 # run the proxy at port 4455
tls:
key:
path: /certs/myapp_root.key
cert:
path: /certs/myapp_root.crt
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- PATCH
- PUT
- GET
- POST
- DELETE
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
allow_credentials: true
max_age: 0
debug: true
api:
port: 4456 # run the api at port 4456
access_rules:
repositories:
- file:///config/rules.json
errors:
fallback:
- json
handlers:
json:
enabled: true
config:
verbose: true
redirect:
enabled: true
config:
to: https://www.ory.sh/docs
mutators:
header:
enabled: true
config:
headers:
X-User: "{{ print .Subject }}"
noop:
enabled: true
authorizers:
allow:
enabled: true
deny:
enabled: true
authenticators:
noop:
enabled: true
anonymous:
enabled: true
config:
subject: guest
oathkeeper rules.json
[
{
"id": "kratos-public",
"version": "v0.36.0-beta.4",
"upstream": {
"url": "http://kratos:4433",
"preserve_host": true,
"strip_path": "/.ory/kratos/public"
},
"match": {
"url": "https://myapp.com/.ory/kratos/public/<.*>",
"methods": [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS"
]
},
"authenticators": [
{
"handler": "noop"
}
],
"authorizer": {
"handler": "allow"
},
"mutators": [
{
"handler": "noop"
}
]
},
{
"id": "kratos-public",
"version": "v0.36.0-beta.4",
"upstream": {
"url": "http://kratos:4434"
},
"match": {
"url": "https://myapp.com/self-service/<.*>",
"methods": [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS"
]
},
"authenticators": [
{
"handler": "noop"
}
],
"authorizer": {
"handler": "allow"
},
"mutators": [
{
"handler": "noop"
}
]
},
{
"id": "allow-frontend",
"version": "v0.36.0-beta.4",
"upstream": {
"url": "http://frontend:4200"
},
"match": {
"url": "https://myapp.com/<(?!(self-service|\\.ory)).*>",
"methods": [
"GET"
]
},
"authenticators": [
{
"handler": "noop"
}
],
"authorizer": {
"handler": "allow"
},
"mutators": [
{
"handler": "noop"
}
]
}
]
Here is what’s happening:
- In Chrome, navigate to
myapp.com/auth/login
.
- The browser then redirects to the Kratos public API:
https://myapp.com/.ory/kratos/public/self-service/browser/flows/login
.
- Kratos redirects the browser back to the application with a requestId:
https://myapp.com/auth/login?request=23c2ae38-30bd-4927-bacc-ef1b514a01f9
Looking at the dev tools, we can see that a cookie is set by Kratos:
csrf_token: tfRs43fajJ04EqumSKz1bfOW02CmI4ktxxSflpWMKPU=
- Kratos API returns a JSON response with a csrf_token:
zqUjPw6C1JVt5u+QWM3PDmAEOZ41Dx1xkuPjwakB7nd7UU/ceVhYCFX0RDYQYTpjk5Lq/pMslFxV93xXPI3Ggg
-
Using this token in the form data, attempt to send a login request:
https://myapp.com/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=23c2ae38-30bd-4927-bacc-ef1b514a01f9
.
-
Note that form data is being sent with Content-Type application/x-www-form-urlencoded
. Also note that the csrf_token sent with the HTML form is different than the one in the cookie set by Kratos during the initial redirect (step 3)
-
The login request fails with the message:
"error": {
"error": {
"code": 400,
"status": "Bad Request",
"reason": "CSRF token is missing or invalid.",
"message": "The request was malformed or contained invalid parameters"
}
}
- Looking at the logs, it appears that Kratos is expecting a csrf_token that is different than either the one it set initially (step 3) or the one sent with the HTML form (step 4)
oathkeeper | [cors] 2020/05/26 14:31:55 Handler: Actual request
oathkeeper | [cors] 2020/05/26 14:31:55 Actual request no headers added: missing origin
kratos_1 | time="2020-05-26T14:31:55Z" level=info msg="started handling request" method=POST name="public#https://myapp.com/.ory/kratos/public/" remote="172.25.0.4:50326" request="/self-service/browser/flows/login/strategies/password?request=20b32d76-ed4d-49d2-993b-1a0016b2855a"
kratos_1 | time="2020-05-26T14:31:55Z" level=warning msg="A request failed due to a missing or invalid csrf_token value" expected_token="jNeIRfLLQeKwqOawWnIPJzHqNfIZqinagChJPRv2Pek5I+SmhRHNf4i6TRYS3vpKwnzmkr+JoPdHPNarjnoVHA==" received_token="zqUjPw6C1JVt5u QWM3PDmAEOZ41Dx1xkuPjwakB7nd7UU/ceVhYCFX0RDYQYTpjk5Lq/pMslFxV93xXPI3Ggg==" received_token_form="zqUjPw6C1JVt5u QWM3PDmAEOZ41Dx1xkuPjwakB7nd7UU/ceVhYCFX0RDYQYTpjk5Lq/pMslFxV93xXPI3Ggg=="
kratos_1 | time="2020-05-26T14:31:55Z" level=error msg="An error occurred while handling a request" code=400 debug= details="map[]" error="The request was malformed or contained invalid parameters" reason="CSRF token is missing or invalid." request-id= status=400 writer=JSON
kratos_1 | time="2020-05-26T14:31:55Z" level=info msg="completed handling request" method=POST name="public#https://myapp.com/.ory/kratos/public/" remote="172.25.0.4:50326" request="/self-service/browser/flows/login/strategies/password?request=20b32d76-ed4d-49d2-993b-1a0016b2855a" status=400 text_status="Bad Request" took="870.1µs"
- As you can see
expected_token="jNeIRfLLQeKwqOawWnIPJzHqNfIZqinagChJPRv2Pek5I+SmhRHNf4i6TRYS3vpKwnzmkr+JoPdHPNarjnoVHA=="
is not the same as either the cookie token or the token in the HTML form:
Cookie in browser, sent in header:
tfRs43fajJ04EqumSKz1bfOW02CmI4ktxxSflpWMKPU=
CSRF_Token sent back with form data:
zqUjPw6C1JVt5u+QWM3PDmAEOZ41Dx1xkuPjwakB7nd7UU/ceVhYCFX0RDYQYTpjk5Lq/pMslFxV93xXPI3Ggg