Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 35 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:

jobs:
test_go:
name: Test
name: Test (go)
strategy:
fail-fast: false
matrix:
Expand All @@ -33,7 +33,7 @@ jobs:
cache: true
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- run: |
make test
make test-go
- uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2
if: ${{ always() }}
with:
Expand Down Expand Up @@ -66,6 +66,39 @@ jobs:
- uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v8
with:
args: --timeout 5000s
test_rs:
name: Test (rust)
strategy:
fail-fast: false
matrix:
platform:
- ubuntu-24.04
- ubuntu-24.04-arm
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev libudev-dev
- name: Setup rust dependencies
uses: taiki-e/install-action@80a23c5ba9e1100fd8b777106e810018ed662a7b # v2
with:
tool: cargo-llvm-cov nextest
- run: make test-rs
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
if: ${{ always() }}
with:
use_oidc: true
flags: go-${{ matrix.platform }}
files: ${{ github.workspace }}/coverage-rs.txt
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
if: ${{ always() }}
with:
use_oidc: true
flags: go-${{ matrix.platform }}
files: ${{ github.workspace }}/coverage-rs.txt
report_type: test_results
lint_rs:
name: Lint (rust)
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ terraform.rc
terraform
examples_local
coverage.*
coverage-rs.*
coverage_in_container.*
junit.xml
authentik_main
Expand Down
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ lint: $(foreach target,$(TARGETS),${target}/lint)
"$(MAKE)" lint-rs
"$(MAKE)" lint-go

test:
test: test-go test-rs

test-go:
go test \
-p 1 \
-v \
Expand All @@ -75,14 +77,25 @@ test:
-out ${PWD}/junit.xml \
-set-exit-code

test-rs:
cargo llvm-cov \
--no-report nextest \
--workspace
cargo llvm-cov report \
--codecov \
--output-path "${PWD}/coverage-rs.txt"
cargo llvm-cov report \
--html \
--output-dir "${PWD}/cache/utils_rs/coverage"

test-integration:
"$(MAKE)" test GO_TEST_FLAGS=-tags=integration
"$(MAKE)" test-go GO_TEST_FLAGS=-tags=integration

test-e2e: containers/e2e/local-build
"$(MAKE)" test GO_TEST_FLAGS=-tags=e2e
"$(MAKE)" test-e2e-convert
"$(MAKE)" test-go GO_TEST_FLAGS=-tags=e2e
"$(MAKE)" test-e2e-go-convert

test-e2e-convert:
test-e2e-go-convert:
go tool covdata textfmt \
-i $(shell find ${PWD}/e2e/coverage/ -mindepth 1 -type d | xargs | sed 's/ /,/g') \
--pkg $(shell go list ./... | grep -v goauthentik.io/platform/vnd | grep -v goauthentik.io/platform/pkg/pb | xargs | sed 's/ /,/g') \
Expand Down
64 changes: 41 additions & 23 deletions pam/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ pub mod token;
pub const PW_PREFIX: &str = "\u{200b}";
pub const PW_PROMPT: &str = "authentik Password: ";

#[derive(Debug, PartialEq, Eq)]
enum AuthenticationAttempt {
Interactive,
Token(String),
}

fn parse_authentication_attempt(password: &str) -> AuthenticationAttempt {
match password.strip_prefix(PW_PREFIX) {
Some(raw_token) => AuthenticationAttempt::Token(raw_token.to_owned()),
None => AuthenticationAttempt::Interactive,
}
}

#[cfg(test)]
mod tests;

pub fn authenticate_impl(
pamh: &mut PamHandle,
_args: Vec<&CStr>,
Expand Down Expand Up @@ -94,29 +110,31 @@ pub fn authenticate_impl(
}
};

if password.starts_with(PW_PREFIX) {
log::debug!("Token authentication");
let raw_token = password.replace(PW_PREFIX, "");
let decoded = match decode_pb::<SshTokenAuthentication>(raw_token) {
Ok(t) => t,
Err(e) => {
log::warn!("failed to decode token: {}", e);
return PamResultCode::PAM_ABORT;
}
};
let token_res = match auth_token(username, decoded.token.to_owned(), bridge) {
Ok(t) => t,
Err(e) => return e,
};
session_data.local_socket = decoded.local_socket;
session_id = token_res.session_id;
} else {
log::debug!("Interactive authentication");
let int_res = match auth_interactive(username, password.to_owned(), &conv, bridge) {
Ok(ss) => ss,
Err(code) => return code,
};
session_id = int_res.session_id;
match parse_authentication_attempt(password) {
AuthenticationAttempt::Token(raw_token) => {
log::debug!("Token authentication");
let decoded = match decode_pb::<SshTokenAuthentication>(raw_token) {
Ok(t) => t,
Err(e) => {
log::warn!("failed to decode token: {}", e);
return PamResultCode::PAM_ABORT;
}
};
let token_res = match auth_token(username, decoded.token.to_owned(), bridge) {
Ok(t) => t,
Err(e) => return e,
};
session_data.local_socket = decoded.local_socket;
session_id = token_res.session_id;
}
AuthenticationAttempt::Interactive => {
log::debug!("Interactive authentication");
let int_res = match auth_interactive(username, password.to_owned(), &conv, bridge) {
Ok(ss) => ss,
Err(code) => return code,
};
session_id = int_res.session_id;
}
}
if !session_data.local_socket.is_empty() {
pam_try_log!(
Expand Down
4 changes: 4 additions & 0 deletions pam/src/auth/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub fn prompt_meta_to_pam_message_style(challenge: &InteractiveChallenge) -> Pam
}
}

#[cfg(test)]
#[path = "interactive_tests.rs"]
mod tests;

pub fn auth_interactive(
username: String,
password: String,
Expand Down
82 changes: 82 additions & 0 deletions pam/src/auth/interactive_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use authentik_sys::generated::sys_auth::{
InteractiveAuthResult, InteractiveChallenge, interactive_challenge::PromptMeta,
};
use pam::constants::{
PAM_BINARY_PROMPT, PAM_ERROR_MSG, PAM_PROMPT_ECHO_OFF, PAM_PROMPT_ECHO_ON, PAM_RADIO_TYPE,
PAM_TEXT_INFO, PamResultCode,
};

use super::{prompt_meta_to_pam_message_style, result_to_pam_result};

fn challenge(prompt_meta: PromptMeta) -> InteractiveChallenge {
InteractiveChallenge {
txid: String::new(),
finished: false,
result: 0,
prompt: String::new(),
prompt_meta: prompt_meta as i32,
debug_info: String::new(),
session_id: String::new(),
component: String::new(),
}
}

#[test]
fn maps_interactive_results_to_pam_codes() {
assert_eq!(
result_to_pam_result(InteractiveAuthResult::PamSuccess as i32),
PamResultCode::PAM_SUCCESS
);
assert_eq!(
result_to_pam_result(InteractiveAuthResult::PamPermDenied as i32),
PamResultCode::PAM_PERM_DENIED
);
assert_eq!(
result_to_pam_result(InteractiveAuthResult::PamAuthErr as i32),
PamResultCode::PAM_AUTH_ERR
);
}

#[test]
fn falls_back_to_system_error_for_unknown_results() {
assert_eq!(result_to_pam_result(999), PamResultCode::PAM_SYSTEM_ERR);
}

#[test]
fn maps_prompt_meta_values_to_pam_styles() {
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamBinaryPrompt)),
PAM_BINARY_PROMPT
);
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamErrorMsg)),
PAM_ERROR_MSG
);
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamPromptEchoOff)),
PAM_PROMPT_ECHO_OFF
);
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamPromptEchoOn)),
PAM_PROMPT_ECHO_ON
);
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamRadioType)),
PAM_RADIO_TYPE
);
assert_eq!(
prompt_meta_to_pam_message_style(&challenge(PromptMeta::PamTextInfo)),
PAM_TEXT_INFO
);
}

#[test]
fn defaults_to_echo_off_for_unknown_prompt_meta() {
let mut unknown = challenge(PromptMeta::Unspecified);
unknown.prompt_meta = 999;

assert_eq!(
prompt_meta_to_pam_message_style(&unknown),
PAM_PROMPT_ECHO_OFF
);
}
27 changes: 27 additions & 0 deletions pam/src/auth/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use super::{AuthenticationAttempt, PW_PREFIX, parse_authentication_attempt};

#[test]
fn parses_interactive_passwords_without_prefix() {
assert_eq!(
parse_authentication_attempt("plain-password"),

Check failure

Code scanning / CodeQL

Hard-coded cryptographic value Critical test

This hard-coded value is used as
a password
.
AuthenticationAttempt::Interactive
);
}

#[test]
fn parses_prefixed_passwords_as_tokens() {
assert_eq!(
parse_authentication_attempt(&format!("{PW_PREFIX}encoded-token")),
AuthenticationAttempt::Token("encoded-token".to_owned())
);
}

#[test]
fn only_strips_the_leading_prefix_marker() {
let password = format!("{PW_PREFIX}token{PW_PREFIX}body");

assert_eq!(
parse_authentication_attempt(&password),
AuthenticationAttempt::Token(format!("token{PW_PREFIX}body"))
);
}
48 changes: 31 additions & 17 deletions pam/src/auth/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@ use authentik_sys::generated::sys_auth::{TokenAuthRequest, TokenAuthResponse};
use authentik_sys::grpc::SysdBridge;
use pam::constants::PamResultCode;

fn validate_token_auth_response(
username: &str,
response: &TokenAuthResponse,
) -> Result<(), PamResultCode> {
if !response.successful {
return Err(PamResultCode::PAM_AUTH_ERR);
}

let token_username = response
.token
.as_ref()
.ok_or(PamResultCode::PAM_AUTH_ERR)?
.preferred_username
.as_str();
if username != token_username {
log::warn!(
"User mismatch: token={:#?}, expected={:#?}",
token_username,
username
);
return Err(PamResultCode::PAM_USER_UNKNOWN);
}

Ok(())
}

#[cfg(test)]
#[path = "token_tests.rs"]
mod tests;

pub fn auth_token(
username: String,
token: String,
Expand All @@ -23,23 +53,7 @@ pub fn auth_token(
}
};

if !response.successful {
return Err(PamResultCode::PAM_AUTH_ERR);
}

validate_token_auth_response(&username, &response)?;
log::debug!("Got valid token: {response:#?}");
let token_username = response
.token
.clone()
.ok_or(PamResultCode::PAM_AUTH_ERR)?
.preferred_username;
if username != token_username {
log::warn!(
"User mismatch: token={:#?}, expected={:#?}",
token_username,
username
);
return Err(PamResultCode::PAM_USER_UNKNOWN);
}
Ok(response)
}
Loading
Loading