-
Notifications
You must be signed in to change notification settings - Fork 5
/
auth_sk.xqm
189 lines (172 loc) · 6.89 KB
/
auth_sk.xqm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
module namespace auth = 'urn:nubisware:muscle:fiber:auth';
import module namespace session = "http://basex.org/modules/session";
declare namespace b64 = "java:java.util.Base64";
declare namespace b64enc = "java:java.util.Base64$Encoder";
declare namespace b64dec = "java:java.util.Base64$Decoder";
declare variable $auth:config := map {
"keycloakurl" : "https://mykeycloak.org",
"realm" : "myrealm",
"clientid" : "myexamplepublicclient",
"client_redirect_uri" : "http://localhost:8984/auth/oidc-callback"
};
declare %private variable $auth:KEYCLOAK_BASE_URL :=
$auth:config?keycloakurl || "/auth/realms/" || $auth:config?realm || "/protocol/openid-connect";
declare %private variable $auth:KEYCLOAK_TOKEN_URL := $auth:KEYCLOAK_BASE_URL || "/token";
declare %private variable $auth:KEYCLOAK_LOGOUT_URL := $auth:KEYCLOAK_BASE_URL || "/logout";
declare %private variable $auth:KEYCLOAK_AUTH_URL := $auth:KEYCLOAK_BASE_URL || "/auth";
declare function auth:hex-to-base64url($tbe as xs:hexBinary) {
auth:bytes-to-base64url(convert:binary-to-bytes($tbe))
};
declare function auth:bytes-to-base64url($tbe as xs:byte*) {
b64enc:encodeToString(b64:getUrlEncoder(), $tbe)
};
declare function auth:extract-tokens-from-jwt($jwt as node()) as map(*){
let $b64decoder := b64:getDecoder()
let $t1 := $jwt/json/access__token/string()
let $t2 := tokenize($t1, "\.")[2]
let $t3 := convert:binary-to-string(convert:integers-to-base64(b64dec:decode($b64decoder, $t2)))
let $at := json:parse($t3)
let $t := $jwt/json/refresh__token/string()
return map{ "accesstoken" : $at, "refreshtoken" : $t, "bearer" : "Bearer " || $t1}
};
(: You can just raise an error with code aut:unauthorized from anywhere in your code to get here and be redirected to keycloak:)
declare
%rest:error("auth:unauthorized")
%rest:error-param("value", "{$value}")
function auth:unauthorized($value as map(*)?) {
web:redirect(web:create-url("/auth/login", $value))
};
(: Start OIDC login with code grant flow this makes it unnecessary to share secrets with a front facing application :)
declare
%rest:path("auth/login")
%rest:GET
%rest:query-param("error", "{$error}")
%rest:query-param("redirect", "{$redirect}", "/")
%output:method("html")
function auth:login-show-ep($error as xs:string?, $redirect as xs:string) {
let $params := map{
"client_id" : $auth:config?clientid, "response_type" : "code", "scope" : "openid",
"state" : ($redirect, "/")[1],
"redirect_uri" : $auth:config?client_redirect_uri
}
return web:redirect(web:create-url($auth:KEYCLOAK_AUTH_URL , $params))
};
(: That's the call back uri you will be redirected back after inserting your credentials in Keycloak :)
declare
%rest:path("auth/oidc-callback")
%rest:GET
%rest:query-param("error", "{$error}")
%rest:query-param("error_description", "{$error-description}")
%rest:query-param("session_state", "{$session-state}")
%rest:query-param("state", "{$state}")
%rest:query-param("code", "{$code}")
%output:method("html")
function auth:oidc-redirects(
$error as xs:string?, $error-description as xs:string?,
$session-state as xs:string?, $code as xs:string?, $state
) {
if(exists($error)) then
error("Unable to connect to auth service: " || $error-description)
else
let $body := web:create-url("",
map{
"grant_type" : "authorization_code",
"code" : $code,
"redirect_uri" : $auth:config?client_redirect_uri,
"client_id" : $auth:config?clientid,
"scope" : "openid"
})
let $token-response := http:send-request(
<http:request method="POST" href="{$auth:KEYCLOAK_TOKEN_URL}">
<http:body media-type="application/x-www-form-urlencoded">{substring-after($body, "?")}</http:body>
</http:request>
)
return
if($token-response[1]/@status != "200") then
error("Unable to authorize")
else
(session:set("principal", ($token-response[2])), web:redirect($state))
};
(: Logout may be just closing the local session but I added also an example for making a backchannel logout thus closing also the SSO session on Keycloak. Note that I am using the original requested url in the state parameter in order to be able to redirect to the requeste d page. Maybe it should be encoded and randomized... :)
declare
%rest:path("auth/logout")
%rest:POST
%rest:form-param("redirect", "{$redirect}", "/")
%output:method("html")
function auth:logout-ep($redirect as xs:string) {
let $tokens := auth:extract-tokens-from-jwt(session:get("principal"))
let $bearer := $tokens?bearer
let $refresh := $tokens?refreshtoken
let $body := web:create-url("",
map{
"refresh_token" : $refresh,
"client_id" : $auth:config?clientid,
"redirect_uri" : $auth:config?client_redirect_uri
})
let $logout := http:send-request(
<http:request method="POST" href="{$auth:KEYCLOAK_LOGOUT_URL}">
<http:header name="Authorization" value="{$bearer}"></http:header>
<http:body media-type="application/x-www-form-urlencoded">{substring-after($body, "?")}</http:body>
</http:request>
)
return
(
session:close(),
web:redirect($redirect)
)
};
(: The following code represents an example application that you can access at http://localhost:8984/authtest.
Two example pages a frontpage and an internal page. Both should be protected :)
declare %private %basex:inline function auth:home-page(){
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Hello Auth Test</h1>
<h2>Principal: {session:get("principal") ! json:serialize(.)}</h2>
<form action="/auth/logout" method="POST">
<input type="hidden" name="redirect" value="/authtest"/>
<input type="submit" name="submit" value="Logout"/>
</form>
</body>
</html>
};
declare %private %basex:inline function auth:internal-page(){
<html>
<head>
<title>Internal</title>
</head>
<body>
<h1>This is an internal page</h1>
<h2>Principal: {json:serialize(session:get("principal"))}</h2>
<a href="/authtest/">Back</a>
<form action="/auth/logout" method="POST">
<input type="hidden" name="redirect" value="/authtest"/>
<input type="submit" name="submit" value="Logout"/>
</form>
</body>
</html>
};
(: This is the permission check on every page of the example application authtest:)
declare %perm:check("/authtest", "{$context}") function auth:access-control($context as map(*)){
let $principal := session:get("principal")
return
if(empty($principal)) then error(xs:QName("auth:unauthorized"),"",map{ "redirect" : $context?path})
else ()
};
(: The example application authtest endpoints:)
declare
%rest:path("/authtest")
%rest:GET
%output:method("html")
function auth:home-page-endpoint() {
auth:home-page()
};
declare
%rest:path("/authtest/internal")
%rest:GET
%output:method("html")
function auth:internal-page-endpoint() {
auth:internal-page()
};