| -- |
| -- Licensed to the Apache Software Foundation (ASF) under one or more |
| -- contributor license agreements. See the NOTICE file distributed with |
| -- this work for additional information regarding copyright ownership. |
| -- The ASF licenses this file to You under the Apache License, Version 2.0 |
| -- (the "License"); you may not use this file except in compliance with |
| -- the License. You may obtain a copy of the License at |
| -- |
| -- http://www.apache.org/licenses/LICENSE-2.0 |
| -- |
| -- Unless required by applicable law or agreed to in writing, software |
| -- distributed under the License is distributed on an "AS IS" BASIS, |
| -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| -- See the License for the specific language governing permissions and |
| -- limitations under the License. |
| -- |
| local string = string |
| local core = require("apisix.core") |
| local ngx_re = require("ngx.re") |
| local openidc = require("resty.openidc") |
| local ngx = ngx |
| local ngx_encode_base64 = ngx.encode_base64 |
| |
| local plugin_name = "openid-connect" |
| |
| |
| local schema = { |
| type = "object", |
| properties = { |
| client_id = {type = "string"}, |
| client_secret = {type = "string"}, |
| discovery = {type = "string"}, |
| scope = { |
| type = "string", |
| default = "openid", |
| }, |
| ssl_verify = { |
| type = "boolean", |
| default = false, |
| }, |
| timeout = { |
| type = "integer", |
| minimum = 1, |
| default = 3, |
| description = "timeout in seconds", |
| }, |
| introspection_endpoint = { |
| type = "string" |
| }, |
| introspection_endpoint_auth_method = { |
| type = "string", |
| default = "client_secret_basic" |
| }, |
| bearer_only = { |
| type = "boolean", |
| default = false, |
| }, |
| realm = { |
| type = "string", |
| default = "apisix", |
| }, |
| logout_path = { |
| type = "string", |
| default = "/logout", |
| }, |
| redirect_uri = { |
| type = "string", |
| description = "use ngx.var.request_uri if not configured" |
| }, |
| public_key = {type = "string"}, |
| token_signing_alg_values_expected = {type = "string"}, |
| set_access_token_header = { |
| description = "Whether the access token should be added as a header to the request " .. |
| "for downstream", |
| type = "boolean", |
| default = true |
| }, |
| access_token_in_authorization_header = { |
| description = "Whether the access token should be added in the Authorization " .. |
| "header as opposed to the X-Access-Token header.", |
| type = "boolean", |
| default = false |
| }, |
| set_id_token_header = { |
| description = "Whether the ID token should be added in the X-ID-Token header to " .. |
| "the request for downstream.", |
| type = "boolean", |
| default = true |
| }, |
| set_userinfo_header = { |
| description = "Whether the user info token should be added in the X-Userinfo " .. |
| "header to the request for downstream.", |
| type = "boolean", |
| default = true |
| } |
| }, |
| required = {"client_id", "client_secret", "discovery"} |
| } |
| |
| |
| local _M = { |
| version = 0.1, |
| priority = 2599, |
| name = plugin_name, |
| schema = schema, |
| } |
| |
| |
| function _M.check_schema(conf) |
| if conf.ssl_verify == "no" then |
| -- we used to set 'ssl_verify' to "no" |
| conf.ssl_verify = false |
| end |
| |
| local ok, err = core.schema.check(schema, conf) |
| if not ok then |
| return false, err |
| end |
| |
| return true |
| end |
| |
| |
| local function get_bearer_access_token(ctx) |
| -- Get Authorization header, maybe. |
| local auth_header = core.request.header(ctx, "Authorization") |
| if not auth_header then |
| -- No Authorization header, get X-Access-Token header, maybe. |
| local access_token_header = core.request.header(ctx, "X-Access-Token") |
| if not access_token_header then |
| -- No X-Access-Token header neither. |
| return false, nil, nil |
| end |
| |
| -- Return extracted header value. |
| return true, access_token_header, nil |
| end |
| |
| -- Check format of Authorization header. |
| local res, err = ngx_re.split(auth_header, " ", nil, nil, 2) |
| |
| if not res then |
| -- No result was returned. |
| return false, nil, err |
| elseif #res < 2 then |
| -- Header doesn't split into enough tokens. |
| return false, nil, "Invalid Authorization header format." |
| end |
| |
| if string.lower(res[1]) == "bearer" then |
| -- Return extracted token. |
| return true, res[2], nil |
| end |
| |
| return false, nil, nil |
| end |
| |
| |
| local function introspect(ctx, conf) |
| -- Extract token, maybe. |
| local has_token, token, err = get_bearer_access_token(ctx) |
| |
| if err then |
| return ngx.HTTP_BAD_REQUEST, err, nil, nil |
| end |
| |
| if not has_token then |
| -- Could not find token. |
| |
| if conf.bearer_only then |
| -- Token strictly required in request. |
| ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. '"' |
| return ngx.HTTP_UNAUTHORIZED, "No bearer token found in request.", nil, nil |
| else |
| -- Return empty result. |
| return nil, nil, nil, nil |
| end |
| end |
| |
| -- If we get here, token was found in request. |
| |
| if conf.public_key then |
| -- Validate token against public key. |
| -- TODO: In the called method, the openidc module will try to extract |
| -- the token by itself again -- from a request header or session cookie. |
| -- It is inefficient that we also need to extract it (just from headers) |
| -- so we can add it in the configured header. Find a way to use openidc |
| -- module's internal methods to extract the token. |
| local res, err = openidc.bearer_jwt_verify(conf) |
| |
| if err then |
| -- Error while validating or token invalid. |
| ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. |
| '", error="invalid_token", error_description="' .. err .. '"' |
| return ngx.HTTP_UNAUTHORIZED, err, nil, nil |
| end |
| |
| -- Token successfully validated. |
| return res, err, token, nil |
| else |
| -- Validate token against introspection endpoint. |
| -- TODO: Same as above for public key validation. |
| local res, err = openidc.introspect(conf) |
| |
| if err then |
| ngx.header["WWW-Authenticate"] = 'Bearer realm="' .. conf.realm .. |
| '", error="invalid_token", error_description="' .. err .. '"' |
| return ngx.HTTP_UNAUTHORIZED, err, nil, nil |
| end |
| |
| -- Token successfully validated and response from the introspection |
| -- endpoint contains the userinfo. |
| return res, err, token, res |
| end |
| end |
| |
| |
| local function add_access_token_header(ctx, conf, token) |
| if token then |
| -- Add Authorization or X-Access-Token header, respectively, if not already set. |
| if conf.set_access_token_header then |
| if conf.access_token_in_authorization_header then |
| if not core.request.header(ctx, "Authorization") then |
| -- Add Authorization header. |
| core.request.set_header(ctx, "Authorization", "Bearer " .. token) |
| end |
| else |
| if not core.request.header(ctx, "X-Access-Token") then |
| -- Add X-Access-Token header. |
| core.request.set_header(ctx, "X-Access-Token", token) |
| end |
| end |
| end |
| end |
| end |
| |
| |
| function _M.rewrite(plugin_conf, ctx) |
| local conf = core.table.clone(plugin_conf) |
| |
| -- Previously, we multiply conf.timeout before storing it in etcd. |
| -- If the timeout is too large, we should not multiply it again. |
| if not (conf.timeout >= 1000 and conf.timeout % 1000 == 0) then |
| conf.timeout = conf.timeout * 1000 |
| end |
| |
| if not conf.redirect_uri then |
| conf.redirect_uri = ctx.var.request_uri |
| end |
| |
| if not conf.ssl_verify then |
| -- openidc use "no" to disable ssl verification |
| conf.ssl_verify = "no" |
| end |
| |
| local response, err |
| |
| if conf.bearer_only or conf.introspection_endpoint or conf.public_key then |
| -- An introspection endpoint or a public key has been configured. Try to |
| -- validate the access token from the request, if it is present in a |
| -- request header. Otherwise, return a nil response. See below for |
| -- handling of the case where the access token is stored in a session cookie. |
| local access_token, userinfo |
| response, err, access_token, userinfo = introspect(ctx, conf) |
| |
| if err then |
| -- Error while validating token or invalid token. |
| core.log.error("OIDC introspection failed: ", err) |
| return response |
| end |
| |
| if response then |
| -- Add configured access token header, maybe. |
| add_access_token_header(ctx, conf, access_token) |
| |
| if userinfo and conf.set_userinfo_header then |
| -- Set X-Userinfo header to introspection endpoint response. |
| core.request.set_header(ctx, "X-Userinfo", |
| ngx_encode_base64(core.json.encode(userinfo))) |
| end |
| end |
| end |
| |
| if not response then |
| -- Either token validation via introspection endpoint or public key is |
| -- not configured, and/or token could not be extracted from the request. |
| |
| -- Authenticate the request. This will validate the access token if it |
| -- is stored in a session cookie, and also renew the token if required. |
| -- If no token can be extracted, the response will redirect to the ID |
| -- provider's authorization endpoint to initiate the Relying Party flow. |
| -- This code path also handles when the ID provider then redirects to |
| -- the configured redirect URI after successful authentication. |
| response, err = openidc.authenticate(conf) |
| |
| if err then |
| core.log.error("OIDC authentication failed: ", err) |
| return 500 |
| end |
| |
| if response then |
| -- If the openidc module has returned a response, it may contain, |
| -- respectively, the access token, the ID token, and the userinfo. |
| -- Add respective headers to the request, if so configured. |
| |
| -- Add configured access token header, maybe. |
| add_access_token_header(ctx, conf, response.access_token) |
| |
| -- Add X-ID-Token header, maybe. |
| if response.id_token and conf.set_id_token_header then |
| local token = core.json.encode(response.id_token) |
| core.request.set_header(ctx, "X-ID-Token", ngx.encode_base64(token)) |
| end |
| |
| -- Add X-Userinfo header, maybe. |
| if response.user and conf.set_userinfo_header then |
| core.request.set_header(ctx, "X-Userinfo", |
| ngx_encode_base64(core.json.encode(response.user))) |
| end |
| end |
| end |
| end |
| |
| |
| return _M |