mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
oauth design (#441)
This commit is contained in:
parent
08cf9a8dc7
commit
152bd79d6d
|
@ -1,167 +0,0 @@
|
||||||
Claims Design
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Claims are a way of associating privileges to sessions, to scope and limit access of an identity.
|
|
||||||
This can be based on what credentials were used to authenticate, by the user requesting claims
|
|
||||||
for a session, or by time limiting claims.
|
|
||||||
|
|
||||||
These tend to fall into three major categories
|
|
||||||
|
|
||||||
Default Interactive Claims
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
When an identity authenticates they are given a set of claims associated to their interactive
|
|
||||||
login. This could include the ability to trigger a claim request or to view personal details.
|
|
||||||
|
|
||||||
Static Application Claims (or lack of)
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
Static application passwords (IE a device imap/smtp password) should have a claim to "email"
|
|
||||||
but not to read personal data and shouldn't be able to change passwords etc.
|
|
||||||
|
|
||||||
Ephemeral Claims
|
|
||||||
----------------
|
|
||||||
|
|
||||||
These are permissions that are limited in time - they *must* be requested and generally require
|
|
||||||
re-authentication with a subset of credentials. This can include the ability to alter ones own
|
|
||||||
account or time limiting admins ability to alter other accounts.
|
|
||||||
|
|
||||||
Detailed Design
|
|
||||||
---------------
|
|
||||||
|
|
||||||
As a result of these scenarios this leads to the following required observations.
|
|
||||||
|
|
||||||
* Claims must be associated to the credentials of the account
|
|
||||||
* Possible claims that could be assigned derive from membership to a group
|
|
||||||
* Claims must be understood by access controls of the server
|
|
||||||
* Claims must be present in user auth tokens for end applications to be able to verify
|
|
||||||
* Two types of claims exist - ephemeral and static
|
|
||||||
|
|
||||||
This leads to a pseudo design such as:
|
|
||||||
|
|
||||||
class: claim
|
|
||||||
name: claim_email
|
|
||||||
claim_name: email
|
|
||||||
member: account_1
|
|
||||||
|
|
||||||
class: claim
|
|
||||||
name: claim_unused
|
|
||||||
claim_name: unused
|
|
||||||
|
|
||||||
class: system, claim
|
|
||||||
name: claim_interactive
|
|
||||||
claim_name: interactive
|
|
||||||
|
|
||||||
class: claim, claim_ephemeral
|
|
||||||
name: claim_alter_self
|
|
||||||
claim_name: alter_self
|
|
||||||
claim_lifetime: 300 # seconds
|
|
||||||
member: account_1
|
|
||||||
|
|
||||||
name: account_1
|
|
||||||
...
|
|
||||||
primary_credential: {
|
|
||||||
type: password|webauthn|password+webauthn
|
|
||||||
claims: [ claim_alter_self ] //note that interactive is implied
|
|
||||||
}
|
|
||||||
application_credentialn: {
|
|
||||||
name: iphone imap password
|
|
||||||
type: generated_password
|
|
||||||
claims: [ claim_email ]
|
|
||||||
}
|
|
||||||
|
|
||||||
When we authenticate with the email password, because there is no lifetime this becomes a static
|
|
||||||
claim:
|
|
||||||
|
|
||||||
UserAuthToken {
|
|
||||||
name: account_1
|
|
||||||
claims: [ email ]
|
|
||||||
}
|
|
||||||
|
|
||||||
If we authenticate with the primary credential, the static claims are initially issued, and because
|
|
||||||
it's the primary token, we get the implied system interactive claim.
|
|
||||||
|
|
||||||
UserAuthToken {
|
|
||||||
name; account_1
|
|
||||||
claims: [ interactive ]
|
|
||||||
}
|
|
||||||
|
|
||||||
To have the "alter_self" claim, we must perform an auth-request-claim operation which re-verifies
|
|
||||||
a credential. This is a subset of the Auth operation
|
|
||||||
|
|
||||||
auth-request-claim ----> verify aci of request (are you interactive? )
|
|
||||||
<---- return challenge
|
|
||||||
send password/cred ----> verify credential
|
|
||||||
<---- update UAT with ephemeral claim
|
|
||||||
|
|
||||||
Then the UserAuthToken would be:
|
|
||||||
|
|
||||||
UserAuthToken {
|
|
||||||
name; account_1
|
|
||||||
claims: [ interactive, alter_self(expire_at_time) ]
|
|
||||||
}
|
|
||||||
|
|
||||||
This means:
|
|
||||||
|
|
||||||
* Consuming applications need to verify the claim list
|
|
||||||
* They need te verify the claim's expiry times.
|
|
||||||
|
|
||||||
For kanidm, to use claims in access controls, these must become filterable elements. On
|
|
||||||
UAT to Entry as part of the event conversion we will perform
|
|
||||||
|
|
||||||
load entry to member
|
|
||||||
for each claim in UAT:
|
|
||||||
if claim is not expired
|
|
||||||
alter memory entry -> add claim
|
|
||||||
|
|
||||||
ACP's can then have filters such as:
|
|
||||||
|
|
||||||
Eq('claim', 'alter_self')
|
|
||||||
|
|
||||||
|
|
||||||
This implies that claim's are in schema to allow filter construction and validation, and in
|
|
||||||
the protected module to prevent their creation.
|
|
||||||
|
|
||||||
|
|
||||||
Questions
|
|
||||||
---------
|
|
||||||
|
|
||||||
We should only be able to request claims on interactive (primary) credential sessions. How should
|
|
||||||
we mark this? I think the UAT needs to retain "what credential id" was used to authenticate, and then
|
|
||||||
emit this to the entry so that it can also be filtered on to determine primary vs application cred.
|
|
||||||
|
|
||||||
Claim and other generated attrs must be system protected, even though they have to exist in schema
|
|
||||||
for filter verification. This likely needs to be added to system_protected plugin to prevent claims
|
|
||||||
from being added to any entry type.
|
|
||||||
|
|
||||||
Once a claim is dynamically added to the entry it must move to a new state that prevents reserialisation to the DB.
|
|
||||||
|
|
||||||
|
|
||||||
Trust Considerations
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Claims should not be replicated, and are auth-silo specific. This is because
|
|
||||||
trusts as designed are about account and group sharing, rather than about detailed privilege or
|
|
||||||
resource granting in the trusting domain.
|
|
||||||
|
|
||||||
Because claims should be associated to groups, we can also apply account pw policy to groups.
|
|
||||||
|
|
||||||
This means that at the very least we have to consider replication of credential metadata though
|
|
||||||
so that the trusting domain can assign the claims somewhere (this metadata will be needed for
|
|
||||||
account cred policy and group membership later anyway). For example:
|
|
||||||
|
|
||||||
spn: claire@domainb
|
|
||||||
class: [trustedaccount, object]
|
|
||||||
trustedcredential: [ name, id, claims ]
|
|
||||||
|
|
||||||
This way when the account authenticates to the trusting domain, because the credential ID that was
|
|
||||||
used is in the UAT, this allows the trusting domain to inspect what credential was used, and to
|
|
||||||
be able to assign it's domain local claims to the session. This could then have a similar work
|
|
||||||
flow when ephemeral claims are needed.
|
|
||||||
|
|
||||||
mental note: because groups will define account policy, when a trusted account is a member of a group
|
|
||||||
and it doesn't meet that groups account policy requirements, it should be listed in the uat as
|
|
||||||
a rejected group so that we can easily diagnose when an account is insufficient to receive that group
|
|
||||||
or that claim as a result. This may affect how we treat memberof on the session though when
|
|
||||||
we do UAT to entry. An argument could be made to strip the memberofs when they are in the rejected
|
|
||||||
list ...
|
|
161
designs/oauth.rst
Normal file
161
designs/oauth.rst
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
Oauth + Scopes + Claims
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Oauth is a web authorisation protocol that allows "single sign on". It's key to note
|
||||||
|
oauth is authorisation, not authentication, as the protocol in it's default forms
|
||||||
|
do not provide identity or authentication information, only information that
|
||||||
|
an entity is authorised for the requested resources.
|
||||||
|
|
||||||
|
Oauth can tie into extensions allowing an identity provider to reveal information
|
||||||
|
about authorised sessions. This extends oauth from an authorisation only system
|
||||||
|
to a system capable of identity and authorisation. Two primary methods of this
|
||||||
|
exist today: rfc7662 token introspection, and openid connect.
|
||||||
|
|
||||||
|
High Level Process
|
||||||
|
------------------
|
||||||
|
|
||||||
|
A client (user) wishes to access a service (resource, resource server). The resource
|
||||||
|
server does not have an active session for the client, so it redirects to the
|
||||||
|
authorisation server to determine if the client should be allowed to proceed, and
|
||||||
|
has the appropriate permissions (scopes).
|
||||||
|
|
||||||
|
The authorisation server checks the current session of the client and may present
|
||||||
|
a login flow if required. Given the identity of the client known to the authorisation
|
||||||
|
sever, and the requested scopes, the authorisation server makes a decision if it
|
||||||
|
allows the authorisation to proceed. The client is also prompted to consent to the
|
||||||
|
authorisation.
|
||||||
|
|
||||||
|
If successful the client is redirected back to the resource server with an authorisation
|
||||||
|
code. The resource server the contacts the authorisation server directly with this
|
||||||
|
code and exchanges it for a valid token that may be provided to the client.
|
||||||
|
|
||||||
|
The resource server may optionally contact the token introspection endpoint about the
|
||||||
|
provided oauth token, which yields extra metadata about the identity that holds the
|
||||||
|
token and completed the authorisation. This metadata may include identity information,
|
||||||
|
but also may include extended metadata, sometimes refered to as "claims". Claims are
|
||||||
|
information bound to a token based on properties of the session that may allow
|
||||||
|
the resource server to make extended authorisation decisions without the need
|
||||||
|
to contact the authorisation server to arbitrate.
|
||||||
|
|
||||||
|
In this model, Kanidm will function as the authorisation server.
|
||||||
|
|
||||||
|
Kanidm UAT Claims
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
To ensure that we can filter and make certain autorisation decisions, the Kanidm UAT
|
||||||
|
needs to be extended with extra claims similar to the token claims. Since we have the
|
||||||
|
ability to strongly type these, we can add these to the UAT. These should include.
|
||||||
|
|
||||||
|
* The UUID of the authenticating credential
|
||||||
|
* The expiry time of this session
|
||||||
|
* the classification of authentication used (MFA, SFA, Cryptographic)
|
||||||
|
* The expiry time of any elevated permissions
|
||||||
|
* If the session is "interactive" IE from a true human rather than an API pw.
|
||||||
|
* If the user is anonymous (?)
|
||||||
|
|
||||||
|
The UAT should be signed with ECDSA so that client applications may inspect the content
|
||||||
|
IE the session expiry time. This may also allow offline validation.
|
||||||
|
|
||||||
|
The ECDSA public key for this should be stored in the Kanidm "domain" configuration. The
|
||||||
|
private key should also be stored in this configuration, but thought will be needed about how
|
||||||
|
to handle this with replication securely IE readonly servers.
|
||||||
|
|
||||||
|
HTTP Endpoints
|
||||||
|
--------------
|
||||||
|
|
||||||
|
We should expose the following endpoints:
|
||||||
|
|
||||||
|
* /oauth/authorise
|
||||||
|
* /oauth/token
|
||||||
|
* /oauth/token/introspect
|
||||||
|
|
||||||
|
All api responses must have:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Cache-Control: no-store
|
||||||
|
Pragma: no-cache
|
||||||
|
|
||||||
|
|
||||||
|
Resource Servers
|
||||||
|
----------------
|
||||||
|
|
||||||
|
For a resource server to work with the authorisation server, it must be a registered
|
||||||
|
application within the authorisation server.
|
||||||
|
|
||||||
|
Each registered resource server will have an associated secret for authentication. The
|
||||||
|
most simple for of this is a "basic" authorisation header.
|
||||||
|
|
||||||
|
This resource server entry will nominially list what scopes map to which kanidm roles,
|
||||||
|
which scopes are "always" available to all authenticated users. Additionally, it may
|
||||||
|
be that we have an extra set of "filter rules" to allow authorisation decisions to be
|
||||||
|
made based on other factors like group membership.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
class: oauth_resource_server
|
||||||
|
class: oauth_resource_server_basic
|
||||||
|
oauth_rs_name: String,
|
||||||
|
oauth_rs_basic_secret: String,
|
||||||
|
# To validate the redirect root
|
||||||
|
oauth_rs_origin: String/URI
|
||||||
|
# Scopes that apply to all users
|
||||||
|
oauth_rs_scope_implicit: String
|
||||||
|
# Scopes that map to groups which will be enforced.
|
||||||
|
oauth_rs_scope_map: (String, reference)
|
||||||
|
# Filter of accounts that may authorise through this.
|
||||||
|
oauth_rs_account_filter: Filter
|
||||||
|
# A per-resource server fernet key for token/codes.
|
||||||
|
# Allows reset per/application in case of suspect compromise.
|
||||||
|
oauth_rs_token_key: String
|
||||||
|
|
||||||
|
The returned authorisation code should be fernet encrypted and contains the unsigned UAT content of the authorised
|
||||||
|
user.
|
||||||
|
|
||||||
|
The provided oauth token for this method will be encrypted with the fernet key of the related
|
||||||
|
resource server. It will contain the unsigned uat of the account in authorised, allowing token
|
||||||
|
introspection/reflection without needing to access the database.
|
||||||
|
|
||||||
|
Token Introspection
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Claims will be mapped to a kanidm namespace. Otherwise the rfc will be followed.
|
||||||
|
|
||||||
|
Security
|
||||||
|
--------
|
||||||
|
|
||||||
|
Only PKCE Oauth 2.0 clients are accepted today. Alternately stronger exchange types may be considered
|
||||||
|
in the future.
|
||||||
|
|
||||||
|
The default filter of accounts will exclude anonymous and tombstones/recycled.
|
||||||
|
|
||||||
|
Should the default filter only allow interactive accounts to participate in this work flow?
|
||||||
|
|
||||||
|
Test Cases / Use Cases
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
roles such as idm_admin/admin should also require claim=sudo to use.
|
||||||
|
|
||||||
|
To change your own details (self write) sudo should be required.
|
||||||
|
|
||||||
|
read_self, mail etc should always be granted.
|
||||||
|
|
||||||
|
Anonymous should not have access to any claims.
|
||||||
|
|
||||||
|
sudo time expiry
|
||||||
|
|
||||||
|
The ability to use oauth should require
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
Oauth2: https://tools.ietf.org/html/rfc6749
|
||||||
|
pkce: https://tools.ietf.org/html/rfc7636
|
||||||
|
token introspection: https://tools.ietf.org/html/rfc7662
|
||||||
|
bearer: https://tools.ietf.org/html/rfc6750
|
||||||
|
device authorisation grant: https://datatracker.ietf.org/doc/html/rfc8628
|
||||||
|
claims ad krb: https://syfuhs.net/2017/07/29/active-directory-claims-and-kerberos-net/
|
||||||
|
openid connect: https://openid.net/developers/specs/
|
||||||
|
|
||||||
|
|
||||||
|
|
32
designs/sudo.rst
Normal file
32
designs/sudo.rst
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
Sudo Mode
|
||||||
|
---------
|
||||||
|
|
||||||
|
To ensure that certain actions are only performed after re-authentication, we should introduce
|
||||||
|
a sudo mode to kanidm. This relies on some changes from Oauth.rst (namely interactive session
|
||||||
|
identification).
|
||||||
|
|
||||||
|
Only interactive sessions (IE not api passwords or radius) must be elligble for sudo mode.
|
||||||
|
|
||||||
|
Sudo mode when requested will perform a partial-reauthentication of the account using a single
|
||||||
|
factor (if mfa). This is determined based on the credential uuid of the associated session.
|
||||||
|
|
||||||
|
When entered, a short sudo expiry timer is attached to the UAT, which is re-issued after the
|
||||||
|
re-authentication.
|
||||||
|
|
||||||
|
During UAT to entry processing for api calls, the sudo timer will be checked. If current
|
||||||
|
time is less than the expiry, then the phantom attribute "sudo" which is boolean, will be set
|
||||||
|
to the entry. If it is not present, or invalid, it will be set to "false".
|
||||||
|
|
||||||
|
This will allow filtering on sudo=true, meaning that certain default access controls can be
|
||||||
|
altered to enforce that they require sudo mode.
|
||||||
|
|
||||||
|
Some accounts by default represent a high level of privilege. These should have implicit sudo
|
||||||
|
granted when they are autheticated. This will be based on a group membership idm_hp_implicit_sudo
|
||||||
|
and should only apply to admin/idm_admin by default. This will pin the sudo expiry to the expiry
|
||||||
|
time of the session (rather than a shorter time).
|
||||||
|
|
||||||
|
Some accounts should never be able to enter sudo mode, and this will be based on the lack of
|
||||||
|
appropriate credentials. IE anonymous can never enter sudo mode, and will always fail. This
|
||||||
|
will allow the removal of a number of hardcoded anonymous exceptions in the IDM server, allowing
|
||||||
|
us to use the acp's to enforce rules instead.
|
||||||
|
|
Loading…
Reference in a new issue