liquid_feedback_frontend
diff app/main/oauth2/token.lua @ 1309:32cc544d5a5b
Cumulative patch for upcoming frontend version 4
author | bsw/jbe |
---|---|
date | Sun Jul 15 14:07:29 2018 +0200 (2018-07-15) |
parents | |
children | 3fcae27c2709 |
line diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/app/main/oauth2/token.lua Sun Jul 15 14:07:29 2018 +0200 1.3 @@ -0,0 +1,274 @@ 1.4 +if not request.is_post() then 1.5 + return execute.view { module = "index", view = "405" } 1.6 +end 1.7 + 1.8 +slot.set_layout(nil, "application/json;charset=UTF-8") 1.9 + 1.10 +request.add_header("Cache-Control", "no-store") 1.11 +request.add_header("Pragma", "no-cache") 1.12 + 1.13 +local function error_result(error_code, error_description) 1.14 + -- TODO special HTTP status codes for some errors? 1.15 + request.set_status("400 Bad Request") 1.16 + slot.put_into("data", json.export{ 1.17 + error = error_code, 1.18 + error_description = error_description 1.19 + }) 1.20 +end 1.21 + 1.22 +local token 1.23 +local grant_type = param.get("grant_type") 1.24 +if grant_type == "authorization_code" then 1.25 + local code = param.get("code") 1.26 + token = Token:by_token_type_and_token("authorization", code) 1.27 +elseif grant_type == "refresh_token" then 1.28 + local refresh_token = param.get("refresh_token") 1.29 + token = Token:by_token_type_and_token("refresh", refresh_token) 1.30 +elseif grant_type == "access_token" then 1.31 + local access_token, access_token_err = util.get_access_token() 1.32 + if access_token_err then 1.33 + if access_token_err == "header_and_param" then 1.34 + return error_result("invalid_request", "Access token passed both via header and param") 1.35 + end 1.36 + error("Error in util.get_access_token") 1.37 + end 1.38 + token = Token:by_token_type_and_token("access", access_token) 1.39 +else 1.40 + return error_result("unsupported_grant_type", "Grant type not supported") 1.41 +end 1.42 + 1.43 +if not token then 1.44 + return error_result("invalid_grant", "Token invalid or expired") 1.45 +end 1.46 + 1.47 +if grant_type == "authorization_code" then 1.48 + if not token.used then 1.49 + local expiry = db:query({"SELECT now() + (? || 'sec')::interval AS expiry", config.oauth2.authorization_code_lifetime }, "object").expiry 1.50 + token.used = true 1.51 + token.expiry = expiry 1.52 + token:save() 1.53 + else 1.54 + token:destroy() 1.55 + return error_result("invalid_grant", "Token invalid or expired") 1.56 + end 1.57 +end 1.58 + 1.59 +if grant_type ~= "access_token" then 1.60 + local cert_ca = request.get_header("X-LiquidFeedback-CA") 1.61 + local cert_distinguished_name = request.get_header("X-SSL-DN") 1.62 + local cert_common_name 1.63 + if cert_distinguished_name then 1.64 + cert_common_name = string.match(cert_distinguished_name, "%f[^/\0]CN=([A-Za-z0-9_.-]+)%f[/\0]") 1.65 + if not cert_common_name then 1.66 + return error_result("invalid_client", "CN in X.509 certificate invalid") 1.67 + end 1.68 + else 1.69 + return error_result("invalid_client", "X.509 client authorization missing") 1.70 + end 1.71 + if token.system_application then 1.72 + if cert_ca ~= "private" then 1.73 + return error_result("invalid_client", "X.509 certificate not signed by private certificate authority or wrong endpoint used") 1.74 + end 1.75 + if cert_common_name ~= token.system_application.cert_common_name then 1.76 + return error_result("invalid_grant", "CN in X.509 certificate incorrect") 1.77 + end 1.78 + else 1.79 + if cert_ca ~= "public" then 1.80 + return error_result("invalid_client", "X.509 certificate not signed by publicly trusted certificate authority or wrong endpoint used") 1.81 + end 1.82 + if cert_common_name ~= token.domain then 1.83 + return error_result("invalid_grant", "CN in X.509 certificate incorrect") 1.84 + end 1.85 + end 1.86 + local client_id = param.get("client_id") 1.87 + if client_id then 1.88 + if token.system_application then 1.89 + if client_id ~= token.system_application.client_id then 1.90 + return error_result("invalid_grant", "Token was issued to another client") 1.91 + end 1.92 + else 1.93 + if client_id ~= "dynamic:" .. token.domain then 1.94 + return error_result ("invalid_grant", "Token was issued to another client") 1.95 + end 1.96 + end 1.97 + elseif grant_type == "authorization_code" and not cert_common_name then 1.98 + return error_result("invalid_request", "No client_id supplied for authorization_code request") 1.99 + end 1.100 +end 1.101 + 1.102 +if grant_type == "authorization_code" then 1.103 + local redirect_uri = param.get("redirect_uri") 1.104 + if (token.redirect_uri_explicit or redirect_uri) and token.redirect_uri ~= redirect_uri then 1.105 + return error_result("invalid_request", "Redirect URI missing or invalid") 1.106 + end 1.107 +end 1.108 + 1.109 +local scopes = { 1.110 + [0] = param.get("scope") 1.111 +} 1.112 +for i = 1, math.huge do 1.113 + scopes[i] = param.get("scope" .. i) 1.114 + if not scopes[i] then 1.115 + break 1.116 + end 1.117 +end 1.118 + 1.119 +if not scopes[0] and #scopes == 0 then 1.120 + for dummy, token_scope in ipairs(token.token_scopes) do 1.121 + scopes[token_scope.index] = token_scope.scope 1.122 + end 1.123 +end 1.124 + 1.125 +local allowed_scopes = {} 1.126 +local requested_detached_scopes = {} 1.127 +for scope in string.gmatch(token.scope, "[^ ]+") do 1.128 + allowed_scopes[scope] = true 1.129 +end 1.130 +for i = 0, #scopes do 1.131 + if scopes[i] then 1.132 + for scope in string.gmatch(scopes[i], "[^ ]+") do 1.133 + if string.match(scope, "_detached$") then 1.134 + requested_detached_scopes[scope] = true 1.135 + end 1.136 + if not allowed_scopes[scope] then 1.137 + return error_result("invalid_scope", "Scope exceeds limits") 1.138 + end 1.139 + end 1.140 + end 1.141 +end 1.142 + 1.143 +local expiry 1.144 + 1.145 +if grant_type == "access_token" then 1.146 + expiry = db:query({ "SELECT FLOOR(EXTRACT(EPOCH FROM ? - now())) AS access_time_left", token.expiry }, "object") 1.147 +else 1.148 + expiry = db:query({ 1.149 + "SELECT now() + (? || 'sec')::interval AS refresh, now() + (? || 'sec')::interval AS access", 1.150 + config.oauth2.refresh_token_lifetime, 1.151 + config.oauth2.access_token_lifetime 1.152 + }, "object") 1.153 +end 1.154 + 1.155 +if grant_type == "refresh_token" then 1.156 + local requested_detached_scopes_list = {} 1.157 + for scope in pairs(requested_detached_scopes) do 1.158 + requested_detached_scopes_list[#requested_detached_scopes_list+1] = scope 1.159 + end 1.160 + local tokens_to_reduce = Token:old_refresh_token_by_token(token, requested_detached_scopes_list) 1.161 + for dummy, t in ipairs(tokens_to_reduce) do 1.162 + local t_scopes = {} 1.163 + for t_scope in string.gmatch(t.scope, "[^ ]+") do 1.164 + t_scopes[t_scope] = true 1.165 + end 1.166 + for scope in pairs(requested_detached_scopes) do 1.167 + local scope_without_detached = string.gmatch(scope, "(.+)_detached") 1.168 + if t_scope[scope] then 1.169 + t_scope[scope] = nil 1.170 + t_scope[scope_without_detached] = true 1.171 + end 1.172 + end 1.173 + local t_scope_list = {} 1.174 + for scope in pairs(t_scopes) do 1.175 + t_scope_list[#t_scope_list+1] = scope 1.176 + end 1.177 + t.scope = table.concat(t_scope_list, " ") 1.178 + t:save() 1.179 + end 1.180 +end 1.181 + 1.182 +local r = json.object() 1.183 + 1.184 +local refresh_token 1.185 +if 1.186 + grant_type ~= "access_token" 1.187 + and (grant_type == "authorization_code" or #(Token:fresh_refresh_token_by_token(token)) == 0) 1.188 +then 1.189 + refresh_token = Token:new() 1.190 + refresh_token.token_type = "refresh" 1.191 + if grant_type == "authorization_code" then 1.192 + refresh_token.authorization_token_id = token.id 1.193 + else 1.194 + refresh_token.authorization_token_id = token.authorization_token_id 1.195 + end 1.196 + refresh_token.member_id = token.member_id 1.197 + refresh_token.system_application_id = token.system_application_id 1.198 + refresh_token.domain = token.domain 1.199 + refresh_token.session_id = token.session_id 1.200 + refresh_token.expiry = expiry.refresh 1.201 + refresh_token.scope = token.scope 1.202 + refresh_token:save() 1.203 + r.refresh_token = refresh_token.token 1.204 +end 1.205 + 1.206 + 1.207 +r.token_type = "bearer" 1.208 +if grant_type == "access_token" then 1.209 + r.expires_in = expiry.access_time_left 1.210 +else 1.211 + r.expires_in = config.oauth2.access_token_lifetime 1.212 +end 1.213 + 1.214 +for i = 0, #scopes do 1.215 + if scopes[i] then 1.216 + local scope = scopes[i] 1.217 + local access_token = Token:new() 1.218 + access_token.token_type = "access" 1.219 + if grant_type == "authorization_code" then 1.220 + access_token.authorization_token_id = token.id 1.221 + else 1.222 + access_token.authorization_token_id = token.authorization_token_id 1.223 + end 1.224 + access_token.member_id = token.member_id 1.225 + access_token.system_application_id = token.system_application_id 1.226 + access_token.domain = token.domain 1.227 + access_token.session_id = token.session_id 1.228 + if grant_type == "access_token" then 1.229 + access_token.expiry = token.expiry 1.230 + else 1.231 + access_token.expiry = expiry.access 1.232 + end 1.233 + access_token.scope = scope 1.234 + access_token:save() 1.235 + if refresh_token then 1.236 + local refresh_token_scope = TokenScope:new() 1.237 + refresh_token_scope.token_id = refresh_token.id 1.238 + refresh_token_scope.index = i 1.239 + refresh_token_scope.scope = scope 1.240 + refresh_token_scope:save() 1.241 + end 1.242 + local index = i == 0 and "" or i 1.243 + r["access_token" .. index] = access_token.token 1.244 + end 1.245 +end 1.246 + 1.247 +r.member_id = token.member_id 1.248 +if token.member.role then 1.249 + r.member_is_role = true 1.250 +end 1.251 +if token.session then 1.252 + r.real_member_id = token.real_member_id 1.253 +end 1.254 + 1.255 +if param.get("include_member", atom.boolean) then 1.256 + if allowed_scopes.identification or allowed_scopes.authentication then 1.257 + local member = token.member 1.258 + r.member = json.object{ 1.259 + id = member.id, 1.260 + name = member.name, 1.261 + } 1.262 + if token.session and token.session.real_member then 1.263 + r.real_member = json.object{ 1.264 + id = token.session.real_member.id, 1.265 + name = token.session.real_member.name, 1.266 + } 1.267 + end 1.268 + if allowed_scopes.identification then 1.269 + r.member.identification = member.identification 1.270 + if token.session and token.session.real_member then 1.271 + r.real_member.identification = token.session.real_member.identification 1.272 + end 1.273 + end 1.274 + end 1.275 +end 1.276 + 1.277 +slot.put_into("data", json.export(r))