1
0

migrate from CAS to Oauth2-JWT Auth (#197)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/197
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-09-08 15:27:28 +02:00
parent bc06001ce9
commit d7a78d0a79
125 changed files with 1537 additions and 1549 deletions
Executable
+279
View File
@@ -0,0 +1,279 @@
#!/bin/bash
if [ "$1" == "--show-password" ]; then
HSADMINNG_JWT_SHOW_PASSWORD=yes
shift
else
HSADMINNG_JWT_SHOW_PASSWORD=
fi
if [ "$1" == "--trace" ]; then
function trace() {
echo "$*" >&2
}
function doCurl() {
set -x
if [ -z "$HSADMINNG_JWT_ASSUME" ]; then
curl --fail-with-body \
--header "Authorization: Bearer $HSADMINNG_JWT_TOKEN" \
"$@"
else
curl --fail-with-body \
--header "Authorization: Bearer $HSADMINNG_JWT_TOKEN" \
--header "assumed-roles: $HSADMINNG_JWT_ASSUME" \
"$@"
fi
set +x
echo
}
shift
else
function trace() {
: # noop
}
function doCurl() {
if [ -z "$HSADMINNG_JWT_ASSUME" ]; then
curl --fail-with-body --header "Authorization: Bearer $HSADMINNG_JWT_TOKEN" "$@"
else
curl --fail-with-body \
--header "Authorization: Bearer $HSADMINNG_JWT_TOKEN" \
--header "assumed-roles: $HSADMINNG_JWT_ASSUME" \
"$@"
echo
fi
}
fi
export HSADMINNG_JWT_ASSUME_HEADER
if [ -f ~/.cas-curl-assume ]; then
HSADMINNG_JWT_ASSUME="$(cat ~/.cas-curl-assume)"
else
HSADMINNG_JWT_ASSUME=
fi
if [ -z "$HSADMINNG_JWT_TOKEN_URL" ]; then
cat >&2 <<EOF
ERROR: environment incomplete
please set the following environment variables:
export HSADMINNG_JWT_TOKEN_URL=https://login.hostsharing.net/oauth/token
export HSADMINNG_JWT_USERNAME=<<optionally, your username, or leave empty after '='>>
export HSADMINNG_JWT_PASSWORD=<<optionally, your password, or leave empty after '='>>
EOF
exit 1
fi
function jwtCurlDocumentation() {
cat <<EOF
curl-wrapper utilizing JWT-authentication for hsadmin-ng
usage: $0 [--trace] [--show-password] <<command>> [parameters]
commands:
EOF
# filters out help texts (containing double-# and following lines with leading single-#) from the commands itself
# (the '' makes sure that this line is not found, just the lines with actual help texts)
sed -n '/#''#/ {x; p; x; s/#''#//; p; :a; n; /^[[:space:]]*#/!b; s/^[[:space:]]*#//; p; ba}' <$0
}
function jwtLogin() {
# JWT token exists and not expired?
if [ -f ~/.jwt-token ]; then
# Check if token is still valid (simple expiry check - you might want to add JWT parsing for more precise validation)
if find ~/.jwt-token -type f -size +0c -mmin -55 2>/dev/null | grep -q .; then
HSADMINNG_JWT_TOKEN=$(<~/.jwt-token)
return
fi
fi
if [ -z "$HSADMINNG_JWT_USERNAME" ]; then
read -e -p "Username: " HSADMINNG_JWT_USERNAME
fi
if [ -z "$HSADMINNG_JWT_PASSWORD" ]; then
read -s -e -p "Password: " HSADMINNG_JWT_PASSWORD
fi
if [ "$HSADMINNG_JWT_SHOW_PASSWORD" == "yes" ]; then
HSADMINNG_JWT_PASSWORD_DISPLAY=$HSADMINNG_JWT_PASSWORD
else
HSADMINNG_JWT_PASSWORD_DISPLAY="<<password hidden - use --show-password to show>>"
fi
# OAuth2 Resource Owner Password Credentials Grant (public client)
trace "+ curl --fail-with-body --show-error -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d \"grant_type=password&username=$HSADMINNG_JWT_USERNAME&password=$HSADMINNG_JWT_PASSWORD_DISPLAY\" \
$HSADMINNG_JWT_TOKEN_URL -o ~/.jwt-token.response"
JWT_RESPONSE=$(curl --fail-with-body --show-error -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "grant_type=password&username=$HSADMINNG_JWT_USERNAME&password=$HSADMINNG_JWT_PASSWORD" \
$HSADMINNG_JWT_TOKEN_URL 2>&1 | tee ~/.jwt-token.response)
# Extract access token from JSON response
HSADMINNG_JWT_TOKEN=$(echo "$JWT_RESPONSE" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
if [ -z "$HSADMINNG_JWT_TOKEN" ]; then
echo "ERROR: could not get JWT access token: $JWT_RESPONSE" >&2
echo >&2
exit 1
fi
echo "$HSADMINNG_JWT_TOKEN" > ~/.jwt-token
trace "JWT Token acquired (length: ${#HSADMINNG_JWT_TOKEN})"
}
function jwtLogout() {
rm -f ~/.jwt-token ~/.jwt-token.response
}
function jwtValidate() {
if [ ! -f ~/.jwt-token ]; then
echo "ERROR: no JWT token found, please login first" >&2
exit 1
fi
HSADMINNG_JWT_TOKEN=$(<~/.jwt-token)
# Simple JWT validation - decode payload (base64 decode the middle part)
JWT_PAYLOAD=$(echo "$HSADMINNG_JWT_TOKEN" | cut -d'.' -f2)
# Add padding if needed for base64 decoding
case $((${#JWT_PAYLOAD} % 4)) in
2) JWT_PAYLOAD="${JWT_PAYLOAD}==" ;;
3) JWT_PAYLOAD="${JWT_PAYLOAD}=" ;;
esac
if command -v base64 >/dev/null 2>&1; then
JWT_DECODED=$(echo "$JWT_PAYLOAD" | base64 -d 2>/dev/null)
if [ $? -eq 0 ]; then
OAUTH_USER=$(echo "$JWT_DECODED" | grep -o '"sub":"[^"]*' | cut -d'"' -f4)
if [ -z "$OAUTH_USER" ]; then
OAUTH_USER=$(echo "$JWT_DECODED" | grep -o '"preferred_username":"[^"]*' | cut -d'"' -f4)
fi
if [ -z "$OAUTH_USER" ]; then
OAUTH_USER=$(echo "$JWT_DECODED" | grep -o '"username":"[^"]*' | cut -d'"' -f4)
fi
if [ -n "$OAUTH_USER" ]; then
echo "OAuth User: $OAUTH_USER"
# Check expiry
EXP=$(echo "$JWT_DECODED" | grep -o '"exp":[0-9]*' | cut -d':' -f2)
if [ -n "$EXP" ]; then
CURRENT_TIME=$(date +%s)
if [ "$EXP" -lt "$CURRENT_TIME" ]; then
echo "WARNING: JWT token has expired" >&2
else
echo "Token expires: $(date -d "@$EXP" 2>/dev/null || date -r "$EXP" 2>/dev/null || echo "unknown")"
fi
fi
else
echo "Could not extract user from JWT token" >&2
fi
else
echo "Could not decode JWT token" >&2
fi
else
echo "JWT token present but cannot validate (base64 command not available)" >&2
fi
}
function jwtToken() {
if [ ! -f ~/.jwt-token ]; then
echo "ERROR: no JWT token found, please login first" >&2
exit 1
fi
HSADMINNG_JWT_TOKEN=$(<~/.jwt-token)
echo "$HSADMINNG_JWT_TOKEN"
}
case "${1,,}" in
# -- generic commands --------------------------------------------------------------------------
""|"-h"|"--help"|"help") ## prints documentation about commands and options
jwtCurlDocumentation
exit
;;
"env") ## prints all related HSADMINNG_JWT_... environment variables; use '--show-password' to show the password as well
# example: jwt-curl -show-password env
echo "export HSADMINNG_JWT_TOKEN_URL=$HSADMINNG_JWT_TOKEN_URL"
echo "export HSADMINNG_JWT_USERNAME=$HSADMINNG_JWT_USERNAME"
if [ "$HSADMINNG_JWT_SHOW_PASSWORD" == "yes" ]; then
echo "export HSADMINNG_JWT_PASSWORD=$HSADMINNG_JWT_PASSWORD"
elif [ -z "$HSADMINNG_JWT_PASSWORD" ]; then
echo "export HSADMINNG_JWT_PASSWORD=#<<not given>>"
else
echo "export HSADMINNG_JWT_PASSWORD=#<<given, but hidden - add --show-password to show>>"
fi
;;
# --- authentication-related commands ------------------------------------------------------------
"login") ## reads username+password and fetches JWT access token (bypasses HSADMINNG_JWT_USERNAME+HSADMINNG_JWT_PASSWORD)
# example: jwt-curl login
jwtLogout
export HSADMINNG_JWT_USERNAME=
export HSADMINNG_JWT_PASSWORD=
jwtLogin
;;
"assume") ## assumes the given comma-separated roles
# example using object-id-name: jwt-curl assume 'hs_office.relation#ExampleMandant-with-PARTNER-ExamplePartner:AGENT'
# example using object-uuid: jwt-curl assume 'hs_office.relation#1d3bc468-c5c8-11ef-9d0d-4751ecfda2b7:AGENT'
shift
if [ -z "$1" ]; then
echo "ERROR: requires comma-separated list of roles to assume" >&2
exit 1
fi
echo "$1" >~/.cas-curl-assume
;;
"unassume") ## do not assume any particular role anymore, use the plain user as RBAC subject
rm -f ~/.cas-curl-assume
;;
"token") ## prints the current JWT access token
jwtToken
;;
"validate") ## validates current JWT token and prints currently logged in user
jwtValidate
;;
"logout") ## logout, deletes JWT token
jwtLogout
;;
# --- HTTP-commands ----------------------------------------------------------------------
"get") ## HTTP GET, add URL as parameter
# example: jwt-curl GET http://localhost:8080/api/hs/office/partners/P-10003 | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
jwtLogin
doCurl "$*"
;;
"post") ## HTTP POST, add curl options to specify the request body and the URL as last parameter
# example: jwt-curl POST \
# -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
# http://localhost:8080/api/test/customers | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
jwtLogin
doCurl --header "Content-Type: application/json" -X POST "$@"
;;
"patch") ## HTTP PATCH, add curl options to specify the request body and the URL as last parameterparameter
# example: jwt-curl PATCH \
# -d '{ "reference":80002 }' \
# http://localhost:8080/api/test/customers/ae90ac2a-4728-4ca9-802e-a0d0108b2324 | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
jwtLogin
doCurl --header "Content-Type: application/json" -X POST "$*"
;;
# --- unknown command --------------------------------------------------------------------
*)
cat >&2 <<EOF
unknown command: '$1'
valid commands: help, login, logout, validate, get, post, patch, delete
EOF
exit 1
;;
esac