quick-addition-90714
09/27/2024, 8:20 AMquick-addition-90714
09/27/2024, 8:23 AMdocker-compose
file that would spin up the services as suggested in the docs:
version: "3.8"
services:
api:
build:
context: .
dockerfile: cmd/api/Dockerfile
ports:
- "3000:3000"
depends_on:
- db
environment:
- DATABASE_URL=<postgres://admin:admin@db:5432/api_db?sslmode=disable>
login:
build:
context: .
dockerfile: cmd/login/Dockerfile
ports:
- "3001:3000"
environment:
- APP_NAME=login-handler
- HYDRA_ADMIN_URL=<http://hydra:4445>
- KRATOS_URL=<http://127.0.0.1:4433>
- KRATOS_INTERNAL_URL=<http://kratos:4433>
depends_on:
- kratos
- hydra
db:
image: postgres:16.4-alpine
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
POSTGRES_DB: api_db
volumes:
- postgres_data:/var/lib/postgresql/data
kratos-db:
image: postgres:16.4-alpine
environment:
POSTGRES_USER: kratos
POSTGRES_PASSWORD: secret
POSTGRES_DB: kratos
volumes:
- kratos_postgres_data:/var/lib/postgresql/data
kratos-migrate:
image: oryd/kratos:v1.2.0
depends_on:
- kratos-db
environment:
- DSN=<postgres://kratos:secret@kratos-db:5432/kratos?sslmode=disable>
volumes:
- type: bind
source: ./ory
target: /etc/config/kratos
command: >
migrate sql -e --yes -c /etc/config/kratos/kratos.yml
restart: on-failure
kratos:
depends_on:
- kratos-migrate
image: oryd/kratos:v1.2.0
ports:
- "4433:4433" # public
- "4434:4434" # admin
restart: unless-stopped
environment:
- DSN=<postgres://kratos:secret@kratos-db:5432/kratos?sslmode=disable>
command: >
serve --dev --watch-courier -c /etc/config/kratos/kratos.yml
volumes:
- type: bind
source: ./ory
target: /etc/config/kratos
hydra-db:
image: postgres:16.4-alpine
environment:
POSTGRES_USER: hydra
POSTGRES_PASSWORD: secret
POSTGRES_DB: hydra
volumes:
- hydra_postgres_data:/var/lib/postgresql/data
hydra-migrate:
image: oryd/hydra:v2.2.0
environment:
- DSN=<postgres://hydra:secret@hydra-db:5432/hydra?sslmode=disable>
command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes
volumes:
- type: bind
source: ./ory
target: /etc/config/hydra
restart: on-failure
depends_on:
- hydra-db
hydra:
image: oryd/hydra:v2.2.0
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user
volumes:
- type: bind
source: ./ory
target: /etc/config/hydra
environment:
- DSN=<postgres://hydra:secret@hydra-db:5432/hydra?sslmode=disable>
- LOG_LEAK_SENSITIVE_VALUES=true
command: serve -c /etc/config/hydra/hydra.yml all --dev
restart: unless-stopped
depends_on:
- hydra-migrate
volumes:
postgres_data:
kratos_postgres_data:
hydra_postgres_data:
This spins up the Kratos and Ory services along with my custom login handler which would provide the users with the custom UI to login. I would later on use it as well for the consent page. (depending on which client initiates the login, first party or third party)
The api container would be my api which would need an access token provided by Hydra.quick-addition-90714
09/27/2024, 8:24 AMurls:
self:
issuer: <http://127.0.0.1:4444>
public: <http://127.0.0.1:4444>
consent: <http://127.0.0.1:3001/consent>
login: <http://127.0.0.1:3001/login>
logout: <http://127.0.0.1:3001/logout>
secrets:
system:
- youReallyNeedToChangeThis
oidc:
subject_identifiers:
supported_types:
- pairwise
- public
pairwise:
salt: youReallyNeedToChangeThis
Along with my Kratos config file:
serve:
public:
base_url: <http://127.0.0.1:4433/>
admin:
base_url: <http://kratos:4434/>
selfservice:
default_browser_return_url: <http://127.0.0.1:3001/>
allowed_return_urls:
- <http://127.0.0.1:3001/>
methods:
password:
enabled: true
flows:
settings:
ui_url: <http://127.0.0.1:3001/settings>
verification:
ui_url: <http://127.0.0.1:3001/verification>
enabled: false
recovery:
ui_url: <http://127.0.0.1:3001/recovery>
enabled: false
logout:
after:
default_browser_return_url: <http://127.0.0.1:3001/login>
login:
ui_url: <http://127.0.0.1:3001/login>
registration:
ui_url: <http://127.0.0.1:3001/registration>
after:
password:
hooks:
- hook: session
error:
ui_url: <http://127.0.0.1:3001/error>
log:
level: debug
hashers:
argon2:
parallelism: 1
memory: 128KB
iterations: 2
salt_length: 16
key_length: 16
identity:
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
oauth2_provider:
url: <http://hydra:4445>
quick-addition-90714
09/27/2024, 8:25 AMlogin-service
I have the following route set up to handle the login functionality:
package api
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
)
var (
hydraAdminUrl = os.Getenv("HYDRA_ADMIN_URL")
kratosUrl = os.Getenv("KRATOS_URL")
kratosInternalUrl = os.Getenv("KRATOS_INTERNAL_URL")
)
type Api struct {
router *http.ServeMux
}
func (api *Api) Router() http.Handler {
return applyMiddleware(api.router, requestLogger, applicationJson)
}
func New() *Api {
api := new(Api)
api.router = http.NewServeMux()
api.router.HandleFunc("/login", api.handleLogin)
return api
}
func (api *Api) handleLogin(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
logAttrs := []slog.Attr{}
for param, values := range queryParams {
for _, value := range values {
logAttrs = append(logAttrs, slog.String(param, value))
}
}
logAttrsAny := make([]any, len(logAttrs))
for i, attr := range logAttrs {
logAttrsAny[i] = attr
}
slog.Debug("Query parameters", logAttrsAny...)
flowId := queryParams.Get("flow")
loginChallenge := queryParams.Get("login_challenge")
if flowId == "" && loginChallenge == "" {
respondWithError(w, http.StatusBadRequest, "The request was malformed, the query parameter should either contain a flow id or a login challenge.")
return
}
if flowId != "" && loginChallenge != "" {
respondWithError(w, http.StatusBadRequest, "The request was malformed, both flow and login_challenge cannot be present simultaneously.")
return
}
if loginChallenge != "" {
kratosBrowserLoginUrl := fmt.Sprintf("%s/self-service/login/browser?login_challenge=%s", kratosUrl, loginChallenge)
http.Redirect(w, r, kratosBrowserLoginUrl, http.StatusFound)
return
}
if flowId != "" {
client := &http.Client{}
kratosLoginUrl := fmt.Sprintf("%s/self-service/login/flows?id=%s", kratosInternalUrl, flowId)
req, err := http.NewRequest("GET", kratosLoginUrl, nil)
if err != nil {
slog.Error("Could not create get request", slog.String("error", err.Error()))
respondWithError(w, http.StatusInternalServerError, "Could not create request to the identity server.")
return
}
resp, err := <http://client.Do|client.Do>(req)
if err != nil {
slog.Error("Failed to connect to identity server", slog.String("error", err.Error()))
respondWithError(w, http.StatusInternalServerError, "Failed to connect to the identity server.")
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respondWithError(w, http.StatusInternalServerError, "Identity service returned an error.")
return
}
// Parse response from Kratos
var kratosResponse map[string]any
if err := json.NewDecoder(resp.Body).Decode(&kratosResponse); err != nil {
slog.Error("Error decoding response from Kratos", slog.String("error", err.Error()))
respondWithError(w, http.StatusInternalServerError, "Failed to parse identity service response.")
return
}
ui, ok := kratosResponse["ui"]
if !ok {
respondWithError(w, http.StatusInternalServerError, "Identity service response does not contain expected UI object.")
return
}
slog.Debug("Kratos UI object", slog.Any("ui", ui))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ui)
return
}
}
This is the part where I got stuck, as I got an error on retrieving the login flow:
login-1 | {"time":"2024-09-27T08:05:23.847220543Z","level":"DEBUG","msg":"Incoming request","method":"GET","path":"/login"}
login-1 | {"time":"2024-09-27T08:05:23.847303668Z","level":"DEBUG","msg":"Query parameters","flow":"e4f66ba2-d812-40b9-8f1f-7a2485d3f9c1"}
kratos-1 | time=2024-09-27T08:05:23Z level=info msg=started handling request http_request=map[headers:map[accept-encoding:gzip user-agent:Go-http-client/1.1] host:kratos:4433 method:GET path:/self-service/login/flows query:Value is sensitive and has been redacted. To see the value set config key "log.leak_sensitive_values = true" or environment variable "LOG_LEAK_SENSITIVE_VALUES=true". remote:172.18.0.10:46510 scheme:http]
kratos-1 | time=2024-09-27T08:05:23Z level=info msg=An error occurred while handling a request audience=application error=map[debug: details:map[docs:<https://www.ory.sh/kratos/docs/debug/csrf> hint:The anti-CSRF cookie was found but the CSRF token was not included in the HTTP request body (csrf_token) nor in the HTTP Header (X-CSRF-Token). reject_reason:The HTTP Cookie Header was set and a CSRF token was sent but they do not match. We recommend deleting all cookies for this domain and retrying the flow.] message:the request was rejected to protect you from Cross-Site-Request-Forgery reason:Please retry the flow and optionally clear your cookies. The request was rejected to protect you from Cross-Site-Request-Forgery (CSRF) which could cause account takeover, leaking personal information, and other serious security issues.
quick-addition-90714
09/27/2024, 8:28 AM5173
and it looks like the csrf cookie was stored to this domain/port instead of my local login api handler. Which I assume is why I'm getting this CSRF error.
I'm using the oauth2 client that was suggested in the docs:
https://github.com/badgateway/oauth2-client
And this is the code that I have in `oauth2.ts`:
import { OAuth2Client } from "@badgateway/oauth2-client";
// TODO: Set these values via variables for environment specific builds
export const client = new OAuth2Client({
server: "<http://localhost:4444>",
clientId: "fcc449ab-d933-4557-a097-8a6e5ff5b865",
tokenEndpoint: "/oauth2/token",
authorizationEndpoint: "/oauth2/auth",
discoveryEndpoint: "/.well-known/oauth2-authorization-server",
});
export const generateRandomState = (): string => {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
};
and in my App.tsx
:
const [pkceCodeVerifier, setPkceCodeVerifier] = useState("");
useEffect(() => {
async function initiateAuthFlow() {
const verifier = await generateCodeVerifier();
setPkceCodeVerifier(verifier);
// TODO: Set these values via variables for environment specific builds
const authUri = await client.authorizationCode.getAuthorizeUri({
redirectUri: "<http://localhost:5173>",
state: generateRandomState(),
codeVerifier: verifier,
scope: ["openid", "offline"],
});
window.location.href = authUri;
}
if (!pkceCodeVerifier) {
initiateAuthFlow();
}
}, [pkceCodeVerifier]);
Which would initiate a login flow when you enter the page.quick-addition-90714
09/27/2024, 8:29 AMquick-addition-90714
09/28/2024, 11:19 AM