Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add POST method for token validation requests #13

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ $ sudo make install
## Configuration

```
auth sufficient pam_oauth2.so <tokeninfo url> <login field> key1=value2 key2=value2
auth sufficient pam_oauth2.so <tokeninfo url> <login field> [POST] [:<Authz payload>] key1=value2 key2=value2
account sufficient pam_oauth2.so
```

Optional parameter `POST` indicates to use POST method when sending request to the `<tokeninfo url>`. This also adds
`Content-Type: application/x-www-form-urlencoded` header and moves CGI parameters from `<tokeninfo url>` into request body.
If this parameter is omitted, the GET method is used.

Optional parameter `:<Authz payload>` specify a payload for `Authorization` HTTP header, e.g. `:dXNlcjpwYXNzd29yZA==` will result
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Should it maybe be more explicit?
Something like:

[Authorization: Basic dXNlcjpwYXNzd29yZA=]

So that it will be possible to also use:

[Authorization: Bearer xyzabcd]

And check for Authorization: prefix instead of just ':'.

According to the pam docs square brackets are needed if argument include space.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I forgot about other authorization schemes that can be used :)
Perhaps if we go with the following syntax:

:<authorisation scheme>:<authorisation payload>

e.g.

:Bearer:xyzabcd

it would be more compact and nice looking. The square brackets would be needed only if the authorisation payload includes spaces.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, it is better to be explicit.
Someone who will have to maintain the configuration would easier understand what is happening just from the first sight if the argument looks exactly as in HTTP header.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The person who have to maintain the configuration will rather be following a defined operational procedure, anyone else will just read the docs. Making it looks like a HTTP header may give a false impression that any other HTTP header can be inserted which is not true.
I have added support for authorisation scheme. Feel free to change it any way you like.

in `Authorization: Basic dXNlcjpwYXNzd29yZA==` header to be added. If this parameter is omitted, no `Authorization` header is added.

## How it works

Lets assume that configuration is looking like:
Expand All @@ -30,7 +37,7 @@ auth sufficient pam_oauth2.so https://foo.org/oauth2/tokeninfo?access_token= uid

And somebody is trying to login with login=foo and token=bar.

pam\_oauth2 module will make http request https://foo.org/oauth2/tokeninfo?access\_token=bar (tokeninfo url is simply concatenated with token) and check response code and content.
pam\_oauth2 module will make http request https://foo.org/oauth2/tokeninfo?access_token=bar (tokeninfo url is simply concatenated with token) and check response code and content.

If the response code is not 200 - authentication will fail. After that it will check response content:

Expand All @@ -55,6 +62,22 @@ It will check that response is a valid JSON object and top-level object contains

If some keys haven't been found or values don't match with expectation - authentication will fail.


### Sample configuration for [Keycloak](https://github.com/keycloak/keycloak)

*As of Keycloak version 24.0.2*

```
auth sufficient pam_oauth2.so http://keycloak.local:8080/realm/master/protocol/openid-connect/token/introspect?token= client_id POST :Y2xpZW50X2lkOnNlY3JldA==
account sufficient pam_oauth2.so http://keycloak.local:8080/realm/master/protocol/openid-connect/token/introspect?token= client_id POST :Y2xpZW50X2lkOnNlY3JldA==
```

*Hint:* you can use the following command to obtain authorisation payload from values of `client_id` and `secret`:

```
echo -n <client_id>:<secret> | base64
```

### Issues and Contributing

Oauth2 PAM module welcomes questions via our [issues tracker](https://github.com/CyberDem0n/pam-oauth2/issues). We also greatly appreciate fixes, feature requests, and updates; before submitting a pull request, please visit our [contributor guidelines](https://github.com/CyberDem0n/pam-oauth2/blob/master/CONTRIBUTING.rst).
Expand Down
49 changes: 41 additions & 8 deletions pam_oauth2.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <syslog.h>
#include <curl/curl.h>
#include <security/pam_modules.h>
Expand Down Expand Up @@ -130,10 +131,14 @@ static int check_response(const struct response token_info, struct check_tokens
return r;
}

static int query_token_info(const char * const tokeninfo_url, const char * const authtok, long *response_code, struct response *token_info) {
static int query_token_info(const int post, const char * authz,
const char * const tokeninfo_url,
const char * const authtok,
long *response_code, struct response *token_info) {
int ret = 1;
char *url;
CURL *session = curl_easy_init();
struct curl_slist *list = NULL;

if (!session) {
syslog(LOG_AUTH|LOG_DEBUG, "pam_oauth2: can't initialize curl");
Expand All @@ -144,9 +149,28 @@ static int query_token_info(const char * const tokeninfo_url, const char * const
strcpy(url, tokeninfo_url);
strcat(url, authtok);

curl_easy_setopt(session, CURLOPT_URL, url);
curl_easy_setopt(session, CURLOPT_WRITEFUNCTION, writefunc);
curl_easy_setopt(session, CURLOPT_WRITEDATA, token_info);
if (post) {
char *postfields = strrchr(url, '?');
if (postfields != NULL) {
*postfields++ = '\0';
curl_easy_setopt(session, CURLOPT_POSTFIELDSIZE, strlen(postfields));
curl_easy_setopt(session, CURLOPT_POSTFIELDS, postfields);
}
curl_easy_setopt(session, CURLOPT_POST, 1L);
}
curl_easy_setopt(session, CURLOPT_URL, url);
if (authz != NULL) {
size_t authz_len = strlen(authz);
{
char header[authz_len + 22];
strcpy(header, "Authorization: Basic ");
strcat(header, authz);
list = curl_slist_append(list, header);
curl_easy_setopt(session, CURLOPT_HTTPHEADER, list);
}
}

if (curl_easy_perform(session) == CURLE_OK &&
curl_easy_getinfo(session, CURLINFO_RESPONSE_CODE, response_code) == CURLE_OK) {
Expand All @@ -156,6 +180,9 @@ static int query_token_info(const char * const tokeninfo_url, const char * const
}

free(url);
if (list != NULL) {
curl_slist_free_all(list);
}
} else {
syslog(LOG_AUTH|LOG_DEBUG, "pam_oauth2: memory allocation failed");
}
Expand All @@ -165,7 +192,9 @@ static int query_token_info(const char * const tokeninfo_url, const char * const
return ret;
}

static int oauth2_authenticate(const char * const tokeninfo_url, const char * const authtok, struct check_tokens *ct) {
static int oauth2_authenticate(const int post, const char *authz,
const char * const tokeninfo_url,
const char * const authtok, struct check_tokens *ct) {
struct response token_info;
long response_code = 0;
int ret;
Expand All @@ -176,7 +205,7 @@ static int oauth2_authenticate(const char * const tokeninfo_url, const char * co
}
token_info.ptr[token_info.len = 0] = '\0';

if (query_token_info(tokeninfo_url, authtok, &response_code, &token_info) != 0) {
if (query_token_info(post, authz, tokeninfo_url, authtok, &response_code, &token_info) != 0) {
ret = PAM_AUTHINFO_UNAVAIL;
} else if (response_code == 200) {
ret = check_response(token_info, ct);
Expand All @@ -191,9 +220,9 @@ static int oauth2_authenticate(const char * const tokeninfo_url, const char * co
}

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
const char *tokeninfo_url = NULL, *authtok = NULL;
const char *tokeninfo_url = NULL, *authtok = NULL, *authz = NULL;
struct check_tokens ct[argc];
int i, ct_len = 1;
int i, ct_len = 1, post = 0;
ct->key = ct->value = NULL;

if (argc > 0) tokeninfo_url = argv[0];
Expand Down Expand Up @@ -225,17 +254,21 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, cons

for (i = 2; i < argc; ++i) {
const char *value = strchr(argv[i], '=');
if (value != NULL) {
if (argv[i][0] == ':') {
authz = &argv[i][1];
} else if (value != NULL) {
ct[ct_len].key = argv[i];
ct[ct_len].key_len = value - argv[i];
ct[ct_len].value = value + 1;
ct[ct_len].value_len = strlen(value + 1);
ct[ct_len++].match = 0;
} else {
post = (strcasecmp("POST", argv[i]) == 0);
}
}
ct[ct_len].key = NULL;

return oauth2_authenticate(tokeninfo_url, authtok, ct);
return oauth2_authenticate(post, authz, tokeninfo_url, authtok, ct);
}

PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) {
Expand Down