Skip to content

feat: Implement authc by app credential - #1187

Open
konac-hamza wants to merge 1 commit into
openstack-experimental:mainfrom
konac-hamza:feature/authc-by-appcred
Open

feat: Implement authc by app credential#1187
konac-hamza wants to merge 1 commit into
openstack-experimental:mainfrom
konac-hamza:feature/authc-by-appcred

Conversation

@konac-hamza

Copy link
Copy Markdown
Collaborator

Summary

Implements authentication by application credential as specified in the
[OpenStack Identity API v3](https://docs.openstack.org/api-ref/identity/v3/index.html#authenticating-with-an-application-credential).

API types: Added ApplicationCredentialAuth and ApplicationCredentialUser
structs to the auth token request types, supporting authentication by credential
ID or by name + user ID.

Provider: Added authenticate_by_application_credential to the
ApplicationCredentialApi trait and ApplicationCredentialService. The method
resolves the credential, applies per-user rate limiting (ADR-0022), verifies the
secret against the stored hash via the backend driver, checks credential
expiration, and validates that the owning user, bound project, and project domain
are all enabled. Added verify_application_credential_secret to the backend
trait with a SQL implementation.

HTTP handler: Extended authenticate_request to dispatch the
"application_credential" method. Extended create_inner to auto-derive the
project scope from the credential's project_id when no explicit scope is
provided, matching the OpenStack API spec behavior.

Error mapping: AuthenticationFailed and ApplicationCredentialExpired
variants now map to HTTP 401.

Test plan

# Backend driver tests (verify_secret: success, wrong secret, not found)
cargo test -p openstack-keystone-appcred-driver-sql
# 15 passed, 0 failed

# Handler unit tests (dispatch by ID, by name, auth failure)
cargo test -p openstack-keystone --lib -- api::v3::auth::token::common::tests
# all passed

# HTTP create unit tests (successful token issuance, auth failed, by name, expired)
cargo test -p openstack-keystone --lib -- api::v3::auth::token::create::tests
# all passed

# Integration tests (success, wrong secret, not found, expired,
# disabled user, disabled project, disabled domain)
cargo test -p test_integration --test integration -- application_credential::authenticate
# 7 passed

# API functional tests (auth by ID, by name, wrong secret, nonexistent,
# disabled user, disabled project, disabled domain, raw 401)
cargo test -p test_api --test integration_api_v3 -- api_v3::auth::token::application_credential
# 8 passed

Security review checklist

  • Does any delegation/authorization decision read the scope where it should read the chain? — No. The auto-derived scope is read from ApplicationCredential.project_id in the authentication result, not from request input.
  • New scope shape or redemption path for a delegated auth? Are effective roles still bounded by the delegation? — Effective roles are bounded by the credential's role list, enforced by the existing ValidatedSecurityContext::new_for_scope path for AuthenticationContext::ApplicationCredential.
  • New ScopeInfo variant or auth method? Updated validate_scope_boundaries(), calculate_effective_roles(), fully_resolved(), and Credentials::try_from? — No new ScopeInfo variant. AuthenticationContext::ApplicationCredential already exists and is handled by all relevant match arms.
  • Does the change let a narrow auth method be broadened by a request-supplied scope? — No. Application credential auth is always bound to the credential's project; the auto-scope derivation only fires when no explicit scope is provided.
  • Are there negative tests proving the escape is blocked? — Yes: wrong secret, nonexistent credential, expired credential, disabled user, disabled project, disabled domain all return authentication errors.

This implementation was developed with AI assistance (Claude).

@konac-hamza
konac-hamza requested a review from gtema August 31, 2026 23:43

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

good work

.map_err(|_| ApplicationCredentialProviderError::AuthenticationFailed)?
.ok_or(ApplicationCredentialProviderError::AuthenticationFailed)?;

if !user.enabled {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good that you thought about that, but it is already present in core-types::auth::UserIdentityInfo::validate to be managed centrally. We should ensure we have an appcred dedicated test though

}

// --- 6. Validate the bound project is enabled ---
let project = state

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is also already enforced in core-types::auth/core::auth

}

// --- 7. Validate the project's domain is enabled ---
let project_domain = state

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

all in all AuthenticationResult verifies that the appcred secret matches and performs rate limiting, but is not doing additional authorization validations. Those are handled by the SecurityContext

async fn authenticate_by_application_credential<'a>(
&self,
ctx: &ExecutionContext<'a>,
id: Option<&'a str>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

one more auth info case is missing: user_name + domain. As such I think it would be better to define an enum which properly lists all necessary combinations: appcred_id, appcred_name + user_id, appcred_name + user_name/domain. It can be e.g.,

struct ApplicationCredentialAuthRequest {
  secret: SecretString,
  application_credential: ApplicationCredentialAuthData
}

enum ApplicationCredentialAuthData {
  Id(ApplicationCredentialAuthById),
  Name(ApplicationCredentialAuthByName)
}

struct ApplicationCredentialAuthById {
  id: String,
  user: Option<UserAuthRef>
}

struct ApplicationCredentialAuthByName {
  name: String,
  user: UserAuthRef
}

struct UserAuthRef {
    pub id: Option<String>,
    pub name: Option<String>,
    pub domain: Option<Domain>,
}

UserAuthRefBuilder can have the validate method enforcing that id or name must be present. While the structure looks much more complex it nicely guarantees during compile time we have all possible combinations covered instead of building a chain of if-else branches. The difference to the type in the api-types is that there we define the json contract while here we want to ensure internally we have the proper information (it is actually possible model the api type with enums, but it looks very weird)

Comment thread crates/api-types/src/v3/auth/token.rs Outdated
serialize_with = "crate::common::serialize_optional_secret"
)]
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
pub secret: Option<SecretString>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it should not be optional - secret is always mandatory and you are enforcing it in the code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@gtema Agreed, secret is mandatory for application credential auth. The issue is that test_post_route_to_target_issues_token sends "application_credential": {"application_credential_id": "tf-alice"} without secret — serde matches the "application_credential" JSON key to the struct field and fails before the route plugin can intercept. Should I add a dummy secret to the test payload to keep secret required, or keep it as Option at the serde layer?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Extend the test please.

Comment thread crates/api-types/src/v3/auth/token.rs Outdated
pub struct ApplicationCredentialUser {
/// User ID.
#[cfg_attr(feature = "validate", validate(length(max = 64)))]
pub id: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as described below, but you can also integrate it here, the ApplicationCredentialUser is specified by ID and/or Name + Domain (which in turn is also specified by ID and/or Name)

feature = "builder",
derive(derive_builder::Builder),
builder(
build_fn(error = "crate::error::BuilderError"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

also as mentioned below I would add here a Builder validator (https://docs.rs/derive_builder/latest/derive_builder/#pre-build-validation) to ensure id or name are present

res.push(auth_res);
} else if method == "application_credential" {
if let Some(app_cred) = &req.auth.identity.application_credential {
let secret = app_cred

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as mentioned above, when secret is not optional in struct this if is a no-op

authenticate_request(state, &req, headers, peer_addr.map(|addr| addr.ip())).await?;
let ctx = SecurityContext::try_from(auth_res)?;

// Application credential auth: auto-derive the project scope from the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

passing scope with appcred is prohibited by python keystone. Also when a token was retrieved using appcred further auth by token must also prohibit setting scope. From my initial intentions this must be already handled in core::auth with different variations, so that you should not bother with the logic here. But please ensure there are dedicated integration tests for this logic

@konac-hamza
konac-hamza force-pushed the feature/authc-by-appcred branch 3 times, most recently from 3bb92cb to 9a842a7 Compare September 3, 2026 00:10
@konac-hamza
konac-hamza requested a review from gtema September 3, 2026 03:38
@konac-hamza
konac-hamza force-pushed the feature/authc-by-appcred branch 2 times, most recently from 897ea41 to ae611c3 Compare September 7, 2026 20:22
Signed-off-by: Hamza Konac <hamza.konac@tubitak.gov.tr>
@konac-hamza
konac-hamza force-pushed the feature/authc-by-appcred branch from ae611c3 to 3be6813 Compare September 7, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants