mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +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