Skip to content

Commit

Permalink
add support for oauth 2.1 with pkce
Browse files Browse the repository at this point in the history
  • Loading branch information
sni committed Jul 22, 2024
1 parent b48481f commit 0b214e2
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 22 deletions.
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
This file documents the revision history for the Monitoring Webinterface Thruk.

next:
- add optional oauth 2.1 pkce workflow
- Config Tool:
- fix validating service parents (#1376)
- enable list wizard for host/service depenencies (#1358)
Expand Down
8 changes: 8 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,14 @@ t/scenarios/auth_oauth/omd/thruk_local.conf
t/scenarios/auth_oauth/README
t/scenarios/auth_oauth/t/100-auth-oauth.t
t/scenarios/auth_oauth/thruk_local.conf
t/scenarios/auth_oauth_pkce/docker-compose.yml
t/scenarios/auth_oauth_pkce/Makefile
t/scenarios/auth_oauth_pkce/omd/Dockerfile
t/scenarios/auth_oauth_pkce/omd/playbook.yml
t/scenarios/auth_oauth_pkce/omd/thruk_local.conf
t/scenarios/auth_oauth_pkce/README
t/scenarios/auth_oauth_pkce/t/100-auth-oauth.t
t/scenarios/auth_oauth_pkce/thruk_local.conf
t/scenarios/auth_ssl_client_cert/docker-compose.yml
t/scenarios/auth_ssl_client_cert/Makefile
t/scenarios/auth_ssl_client_cert/omd/cgi.cfg
Expand Down
1 change: 1 addition & 0 deletions docs/documentation/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,7 @@ ex.:
token_url = https://oauthserver/oauth2/v1/token # URL to exchange code into token
api_url = https://oauthserver/oauth2/v1/userinfo # API endpoint to retrieve user information from
login_field = login # Hash key from userinfo to get the actual username from. If not set, Thruk will try 'login', then 'email'
#enable_pkce = 0 # enable oauth 2.1 pkce workflow if set to 1. Disable by default
</provider>
</auth_oauth>

Expand Down
16 changes: 16 additions & 0 deletions lib/Thruk/Utils/Log.pm
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use Thruk::Utils::Encode ();
use base 'Exporter';
our @EXPORT_OK = qw(_fatal _error _warn _info _infos _infoc
_debug _debug2 _debugs _debugc _trace _audit_log
_debug_http_response
);
our %EXPORT_TAGS = ( all => \@EXPORT_OK );

Expand Down Expand Up @@ -115,6 +116,21 @@ sub _debug2 {
return &_log(DEBUG2, \@_);
}

##############################################
sub _debug_http_response {
my($res) = @_;
_debug("request:");
_debug(">>>");
_debug($res->request->as_string());
_debug("<<< end of request");
_debug("\n\nresponse:\n");
_debug(">>>");
_debug($res->as_string());
_debug("<<< end of response");

return;
}

##############################################
sub _log {
my($lvl, $data, $options) = @_;
Expand Down
99 changes: 77 additions & 22 deletions lib/Thruk/Utils/OAuth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use warnings;
use strict;
use Cpanel::JSON::XS qw/decode_json/;
use Data::Dumper;
use Digest::SHA ();
use MIME::Base64 ();

use Thruk::Authentication::User ();
use Thruk::Controller::login ();
Expand Down Expand Up @@ -57,14 +59,20 @@ sub handle_oauth_login {
my $ua = Thruk::UserAgent->new({}, $c->config);
$ua->default_header(Accept => "application/json");
_debug(sprintf("oauth login step2: fetching token from: %s", $auth->{'token_url'})) if Thruk::Base->debug;
my $res = $ua->post($auth->{'token_url'}, {
client_id => $auth->{'client_id'},
client_secret => $auth->{'client_secret'},
code => $code,
redirect_uri => $loginpage_uri,
state => $state,
grant_type => 'authorization_code',
});
my $token_data = {
client_id => $auth->{'client_id'},
client_secret => $auth->{'client_secret'},
code => $code,
redirect_uri => $loginpage_uri,
state => $state,
grant_type => 'authorization_code',
};
if($auth->{'enable_pkce'}) {
$token_data->{'code_verifier'} = $data->{'pkce_code'};
delete $token_data->{'client_secret'}; # client secret is not required when using pkce
}
my $res = $ua->post($auth->{'token_url'}, $token_data);
_debug_http_response($res) if Thruk::Base->trace;
unlink($auth_folder."/".$state.".json");
my $token = _get_json($c, $res);
if(!$token || !$token->{"access_token"}) {
Expand All @@ -81,14 +89,12 @@ sub handle_oauth_login {
if ($auth->{'api_url'}) {
_debug(sprintf("oauth login step2: fetching user id from: %s", $auth->{'api_url'})) if Thruk::Base->debug;
$res = $ua->get($auth->{'api_url'});
_debug_http_response($res) if Thruk::Base->trace;
my $userinfo = _get_json($c, $res);
if(!$userinfo) {
return $c->detach_error({msg => "cannot fetch oauth user details", code => 500, debug_information => { res => $res }});
}
_debug(sprintf("oauth login step2: got user details:")) if Thruk::Base->debug;
_debug(Dumper($userinfo)) if Thruk::Base->debug;
# get username from response hash
$login = $auth->{'login_field'} ? $userinfo->{$auth->{'login_field'}} : ($userinfo->{'login'} || $userinfo->{'email'});
$login = _extract_login($auth, $userinfo);
if(!defined $login) {
return $c->detach_error({msg => "cannot find oauth user name", code => 500, debug_information => { userinfo => $userinfo }});
}
Expand All @@ -106,6 +112,7 @@ sub handle_oauth_login {
if ($auth->{'jwks_url'}) {
_debug(sprintf("oauth login step2: get jwks from: %s", $auth->{'jwks_url'})) if Thruk::Base->debug;
$res = $ua->get($auth->{'jwks_url'});
_debug_http_response($res) if Thruk::Base->trace;
my $jwks = _get_json($c, $res);
$id_token = decode_jwt(token => $token->{'id_token'}, kid_keys => $jwks);
} elsif ($auth->{'jwk_key'}) {
Expand All @@ -114,8 +121,7 @@ sub handle_oauth_login {
_debug("oauth login step2: WARNING insecure JWT decode");
$id_token = decode_jwt(token => $token->{'id_token'}, ignore_signature => 1);
}
_debug(Dumper($id_token)) if Thruk::Base->debug;
$login = $auth->{'login_field'} ? $id_token->{$auth->{'login_field'}} : ($id_token->{'login'} || $id_token->{'email'});
$login = _extract_login($auth, $id_token);
if(!defined $login) {
return $c->detach_error({msg => "cannot find oauth user name", code => 500, debug_information => { token => $token, id_token => $id_token }});
}
Expand Down Expand Up @@ -144,16 +150,33 @@ sub handle_oauth_login {
}
# redirect to auth url
$state = Thruk::Utils::Crypt::random_uuid([time()]);

my $state_data = {
'time' => time(),
'oauth' => $id,
'referer' => $referer,
};
my $login_data = {
client_id => $auth->{'client_id'},
scope => $auth->{'scopes'},
state => $state,
response_type => 'code',
redirect_uri => $loginpage_uri,
};

# PKCE workflow as described in rfc7636
if($auth->{'enable_pkce'}) {
my $pkce_code = substr(Thruk::Utils::Crypt::random_uuid([time()]), 0, 64);
my $pkce_code_challenge = _base64_url_encode(Digest::SHA::sha256($pkce_code));
$login_data->{'code_challenge'} = $pkce_code_challenge;
$login_data->{'code_challenge_method'} = 'S256';
$state_data->{'pkce_code'} = $pkce_code;
}

Thruk::Utils::IO::mkdir($auth_folder);
Thruk::Utils::IO::json_lock_store($auth_folder."/".$state.".json", { 'time' => time(), 'oauth' => $id, referer => $referer });
Thruk::Utils::IO::json_lock_store($auth_folder."/".$state.".json", $state_data);
_cleanup_oauth_files($auth_folder, 600);
my $oauth_login_url = Thruk::Utils::Filter::uri_with($c, {
client_id => $auth->{'client_id'},
scope => $auth->{'scopes'},
state => $state,
response_type => 'code',
redirect_uri => $loginpage_uri,
}, 1, $auth->{'auth_url'}, 1);
my $oauth_login_url = Thruk::Utils::Filter::uri_with($c, $login_data, 1, $auth->{'auth_url'}, 1);
_debug("oauth login step1: redirecting to ".$oauth_login_url) if Thruk::Base->verbose;
return $c->redirect_to($oauth_login_url);
}
Expand Down Expand Up @@ -201,4 +224,36 @@ sub _get_json {
return $c->detach_error({msg => "cannot get oauth data", code => 500, debug_information => { res => $res }});
}

##########################################################
# get username from response hash
sub _extract_login {
my($auth, $userinfo) = @_;

if(Thruk::Base->debug) {
_debug("oauth login step2: got user details:");
_debug($userinfo);
}

if($auth->{'login_field'}) {
return $userinfo->{$auth->{'login_field'}};
}

return $userinfo->{'login'} if $userinfo->{'login'};
return $userinfo->{'email'} if $userinfo->{'email'};

return;
}

##########################################################
sub _base64_url_encode {
my($str) = @_;

my $data = MIME::Base64::encode_base64($str, '');
$data =~ tr|+/=|-_|d;

return($data);
}

##########################################################

1;
1 change: 1 addition & 0 deletions t/scenarios/auth_oauth_pkce/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_run
8 changes: 8 additions & 0 deletions t/scenarios/auth_oauth_pkce/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include ../_common/Makefile.common

wait_start:
for x in $$(seq $(STARTUPWAIT)); do \
if [ $$(docker compose logs | grep "failed=" | grep -v "failed=0" | wc -l) -gt 0 ]; then $(MAKE) wait_start_failed; exit 1; fi; \
if [ $$(curl -ks http://127.0.0.3:60080/demo/thruk/cgi-bin/remote.cgi | grep -c OK) -gt 0 ]; then break; else sleep 1; fi; \
if [ $$x -eq $(STARTUPWAIT) ]; then $(MAKE) wait_start_failed; exit 1; fi; \
done
6 changes: 6 additions & 0 deletions t/scenarios/auth_oauth_pkce/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
== OAuth Setup

to manually reproduce things here you can:

- open firefox and connect to: https://192.168.105.2/demo/

38 changes: 38 additions & 0 deletions t/scenarios/auth_oauth_pkce/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
networks:
default:
ipam:
config:
- subnet: 192.168.105.0/24

services:
omd:
build: omd/
ports:
- "127.0.0.3:60080:80" # apache
- "127.0.0.3:60557:6557" # livestatus
- "127.0.0.3:8003:8003" # grafana
- "127.0.0.3:4444:4444" # oauth server
volumes:
- ../../../:/thruk:ro
- .:/scenario:ro
networks:
default:
ipv4_address: 192.168.105.2
depends_on:
- mock-oauth2-server

mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:2.1.8
ports:
- 8080:8080
environment:
JSON_CONFIG: '{ "interactiveLogin": false }'
networks:
default:
ipv4_address: 192.168.105.3
healthcheck:
test: ["CMD", "curl", "-kf", "http://localhost:8080/default/.well-known/openid-configuration"]
interval: 10s
timeout: 10s
retries: 3
start_period: 5m
5 changes: 5 additions & 0 deletions t/scenarios/auth_oauth_pkce/omd/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM local/thruk-labs-rocky:nightly

COPY playbook.yml /root/ansible_dropin/
ENV ANSIBLE_ROLES_PATH /thruk/t/scenarios/_common/ansible/roles
COPY thruk_local.conf /root/
31 changes: 31 additions & 0 deletions t/scenarios/auth_oauth_pkce/omd/playbook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
- hosts: all
roles:
- role: common
- role: thruk_developer
tasks:
- name: "omd config change"
shell: "omd config demo change"
args:
stdin: |
APACHE_MODE=own
- copy:
src: /omd/sites/demo/share/doc/naemon/example.cfg
dest: /omd/sites/demo/etc/naemon/conf.d/example.cfg
owner: demo
group: demo
- copy:
src: /root/thruk_local.conf
dest: /omd/sites/demo/etc/thruk/thruk_local.conf
owner: demo
group: demo
- name: "wait for oauth server to come up"
uri:
url: "http://192.168.105.3:8080/default/.well-known/openid-configuration"
validate_certs: False
status_code: 200
register: result
until: result.status == 200
retries: 180
delay: 1

14 changes: 14 additions & 0 deletions t/scenarios/auth_oauth_pkce/omd/thruk_local.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<auth_oauth>
<provider>
name = Login with OAuth
client_id = clientö
client_secret = secret
scopes = openid profile email
# get urls from http://192.168.105.3:8080/default/.well-known/openid-configuration
auth_url = http://192.168.105.3:8080/default/authorize
token_url = http://192.168.105.3:8080/default/token
api_url = http://192.168.105.3:8080/default/userinfo
login_field = azp
enable_pkce = 1
</provider>
</auth_oauth>
48 changes: 48 additions & 0 deletions t/scenarios/auth_oauth_pkce/t/100-auth-oauth.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use warnings;
use strict;
use Test::More;
use utf8;

BEGIN {
use lib('t');
require TestUtils;
import TestUtils;
plan tests => 58;
}

###############################################################################
TestUtils::test_page(
'url' => '/thruk/cgi-bin/login.cgi?logout',
'like' => ['Login with OAuth'],
'code' => 401,
'follow' => 1,
);

###############################################################################
TestUtils::test_page(
'url' => '/thruk/cgi-bin/login.cgi',
'like' => ['Login with OAuth'],
'code' => 401,
);

###############################################################################
TestUtils::test_page(
'url' => '/thruk/cgi-bin/login.cgi',
'post' => { 'oauth' => 0, submit => 'login' },
'like' => ['tac.cgi'],
'follow' => 1,
);

###############################################################################
TestUtils::test_page(
'url' => '/thruk/cgi-bin/tac.cgi',
'like' => ['>User<.*?>clientö<'],
);

###############################################################################
TestUtils::test_page(
'url' => '/thruk/cgi-bin/login.cgi?logout',
'like' => ['Login with OAuth'],
'code' => 401,
'follow' => 1,
);
27 changes: 27 additions & 0 deletions t/scenarios/auth_oauth_pkce/thruk_local.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Component Thruk::Backend>
<peer>
name = naemon
id = abcd
type = livestatus
<options>
peer = 192.168.105.2:6557
</options>
</peer>
</Component>

<auth_oauth>
<provider>
name = Login with OAuth
client_id = clientö
client_secret = secret
scopes = openid profile email
# get urls from http://192.168.105.3:8080/default/.well-known/openid-configuration
auth_url = http://192.168.105.3:8080/default/authorize
token_url = http://192.168.105.3:8080/default/token
api_url = http://192.168.105.3:8080/default/userinfo
login_field = azp
enable_pkce = 1
</provider>
</auth_oauth>

slow_page_log_threshold = 45

0 comments on commit 0b214e2

Please sign in to comment.