Skip to content

Commit c232170

Browse files
committed
Add a Karate feature for apiKeyAuthMechanism
1 parent ebe08e0 commit c232170

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
@ignore
2+
Feature: Prepare API keys for the api-key-auth feature
3+
4+
# The hashes below are sha256 of the key strings, which is what
5+
# mongoApiKeyAuthenticator stores — the key itself never is:
6+
#
7+
# printf 'rhak_valid' | shasum -a 256
8+
# printf 'rhak_noroles' | shasum -a 256
9+
# printf 'rhak_expired' | shasum -a 256
10+
# printf 'rhak_poweruser' | shasum -a 256
11+
#
12+
# Keys live in a database of their own, created here db -> collection ->
13+
# documents. Not restheart-test: that one holds users and acl and is not a place
14+
# to be creating arbitrary collections in.
15+
#
16+
# The documents are written with PUT and `wm=upsert`. Without the write mode a
17+
# PUT on a document that does not exist yet answers 404, since the default mode
18+
# updates rather than creates — and upsert is also what makes this setup
19+
# re-runnable, where a POST would add four more keys on every run.
20+
21+
Background:
22+
* url 'http://localhost:8080'
23+
* def basic =
24+
"""
25+
function(creds) {
26+
var temp = creds.username + ':' + creds.password;
27+
var Base64 = Java.type('java.util.Base64');
28+
var encoded = Base64.getEncoder().encodeToString(temp.toString().getBytes());
29+
return 'Basic ' + encoded;
30+
}
31+
"""
32+
33+
* def admin = basic({username: 'admin', password: 'secret'})
34+
35+
Scenario: Create the test-apikeys db and seed the keys
36+
37+
* header Authorization = admin
38+
Given path 'test-apikeys'
39+
When method PUT
40+
Then assert responseStatus == 201 || responseStatus == 200
41+
42+
* header Authorization = admin
43+
Given path 'test-apikeys/keys'
44+
When method PUT
45+
Then assert responseStatus == 201 || responseStatus == 200
46+
47+
# A working key. `admin` is the role the test ACL grants path-prefix "/" to,
48+
# so this one can reach /secho.
49+
* header Authorization = admin
50+
Given path 'test-apikeys/keys/valid'
51+
And param wm = 'upsert'
52+
And request
53+
"""
54+
{
55+
"hash": "cff1ff8ac100e099a885d9079ebe17919408763bca38289196d38490f335ff42",
56+
"user": "admin",
57+
"roles": ["admin"]
58+
}
59+
"""
60+
When method PUT
61+
Then assert responseStatus == 201 || responseStatus == 200
62+
63+
# Same principal, no roles at all. Deny-by-default made observable: the
64+
# request authenticates and then reaches nothing.
65+
* header Authorization = admin
66+
Given path 'test-apikeys/keys/noroles'
67+
And param wm = 'upsert'
68+
And request
69+
"""
70+
{
71+
"hash": "a6c408d4deabb91ca3e358f28ba7e21526249bb352e82696958b0bc30924fd45",
72+
"user": "admin",
73+
"roles": []
74+
}
75+
"""
76+
When method PUT
77+
Then assert responseStatus == 201 || responseStatus == 200
78+
79+
# Expired an hour ago. Present in the collection on purpose: a TTL index
80+
# reclaims lazily, so the authenticator has to refuse it on its own.
81+
* header Authorization = admin
82+
Given path 'test-apikeys/keys/expired'
83+
And param wm = 'upsert'
84+
And request
85+
"""
86+
{
87+
"hash": "c8f0f2bb100cea93d40356502f09746413f4ebcffc2b2c411cdd9657d0af23c8",
88+
"user": "admin",
89+
"roles": ["admin"],
90+
"expiresAt": { "$date": 1000000000000 }
91+
}
92+
"""
93+
When method PUT
94+
Then assert responseStatus == 201 || responseStatus == 200
95+
96+
# A narrower role. The test ACL grants poweruser only GET /testdb, so this key
97+
# is what shows that the *specific* roles on the key document drive
98+
# authorization — not merely that some role arrived.
99+
* header Authorization = admin
100+
Given path 'test-apikeys/keys/poweruser'
101+
And param wm = 'upsert'
102+
And request
103+
"""
104+
{
105+
"hash": "ca8dcb2d445daf0ce1415b6e3a87c5da3bf5811281dfd87592d61569e8d9f994",
106+
"user": "admin",
107+
"roles": ["poweruser"]
108+
}
109+
"""
110+
When method PUT
111+
Then assert responseStatus == 201 || responseStatus == 200
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
Feature: test apiKeyAuthMechanism
2+
3+
# What the unit tests cannot reach.
4+
#
5+
# ApiKeyAuthMechanismTest exercises the mechanism against a stub authenticator,
6+
# in isolation. The claim the whole design rests on is a different one: that this
7+
# mechanism and jwtAuthenticationMechanism share the Bearer scheme in a running
8+
# instance, in the right order. That order comes from @RegisterPlugin(priority)
9+
# via PluginsFactory -> PluginsRegistryImpl -> AuthenticatorMechanismsHandler,
10+
# and was established by reading the code rather than by observing it.
11+
#
12+
# If that reasoning were wrong, every unit test would still pass and the feature
13+
# would be broken — the JWT mechanism would answer NOT_AUTHENTICATED for a key
14+
# and end the chain. Hence the first two scenarios below, which are the point of
15+
# this file; the rest is behaviour that is cheaper to pin here than to mock.
16+
17+
Background:
18+
* callonce read('api-key-auth-setup.feature')
19+
* url 'http://localhost:8080'
20+
21+
# roles: [admin], alg=HS256, key=secret — the same token jwt-auth.feature uses
22+
* def jwt = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJyZXN0aGVhcnQub3JnIiwicm9sZXMiOlsiYWRtaW4iXSwiZXh0cmEiOnsiYSI6MSwiYiI6Mn19.vDaJOoPH5EnAiyM6lF737vqgi978S2GIAQe1gq33eDU'
23+
24+
Scenario: a JWT still authenticates with apiKeyAuthMechanism enabled ahead of it
25+
# The regression that matters. Enabling a mechanism that also reads Bearer
26+
# must not change the outcome of a request that carries a JWT.
27+
* header Authorization = jwt
28+
Given path '/secho'
29+
When method GET
30+
Then status 200
31+
32+
Scenario: an API key authenticates under the same Bearer scheme
33+
* header Authorization = 'Bearer rhak_valid'
34+
Given path '/secho'
35+
When method GET
36+
Then status 200
37+
38+
Scenario: the specific roles on the key drive authorization
39+
# The test ACL grants poweruser GET /testdb and nothing else, so this key
40+
# reaching one and not the other shows the roles came from the key document
41+
# rather than from the user it belongs to — that user is `admin`, who would
42+
# have reached both.
43+
* header Authorization = 'Bearer rhak_poweruser'
44+
Given path '/testdb'
45+
When method GET
46+
Then assert responseStatus == 200 || responseStatus == 404
47+
48+
* header Authorization = 'Bearer rhak_poweruser'
49+
Given path '/secho'
50+
When method GET
51+
Then status 403
52+
53+
Scenario: a key naming no roles authenticates but reaches nothing
54+
# Deny-by-default, end to end: authentication succeeds — so this is 403 and
55+
# not 401 — and then the ACL matches nothing.
56+
* header Authorization = 'Bearer rhak_noroles'
57+
Given path '/secho'
58+
When method GET
59+
Then status 403
60+
61+
Scenario: an expired key is refused even though it is still in the collection
62+
* header Authorization = 'Bearer rhak_expired'
63+
Given path '/secho'
64+
When method GET
65+
Then status 401
66+
67+
Scenario: an unknown key with the prefix is refused rather than passed on
68+
* header Authorization = 'Bearer rhak_nosuchkey'
69+
Given path '/secho'
70+
When method GET
71+
Then status 401
72+
73+
Scenario: a Bearer value with no prefix is left to the JWT mechanism
74+
# Not ours, so we decline; the JWT mechanism owns the answer and the answer
75+
# is 401. What must not happen is this reaching the key lookup.
76+
* header Authorization = 'Bearer not-a-token'
77+
Given path '/secho'
78+
When method GET
79+
Then status 401
80+
81+
Scenario: no Authorization header at all
82+
Given path '/secho'
83+
When method GET
84+
Then status 401

core/src/test/resources/etc/conf-overrides.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@
1414
/jwtAuthenticationMechanism/rolesClaim: roles
1515
/jwtAuthenticationMechanism/base64Encoded: false
1616

17+
# apiKeyAuthMechanism shares the Bearer scheme with jwtAuthenticationMechanism: it runs
18+
# first (priority) and declines, with NOT_ATTEMPTED, anything not carrying the prefix.
19+
# Both are enabled here so api-key-auth.feature can prove they actually compose in a
20+
# running instance — the one claim the unit tests structurally cannot reach.
21+
/apiKeyAuthMechanism/enabled: true
22+
/apiKeyAuthMechanism/authenticator: mongoApiKeyAuthenticator
23+
/apiKeyAuthMechanism/prefix: rhak_
24+
25+
/mongoApiKeyAuthenticator/enabled: true
26+
/mongoApiKeyAuthenticator/keys-db: test-apikeys
27+
/mongoApiKeyAuthenticator/keys-collection: keys
28+
# Off so the feature seeds keys and uses them immediately, and so a revocation test
29+
# would not have to wait out a TTL.
30+
/mongoApiKeyAuthenticator/cache-enabled: false
31+
1732
/jwtConfigProvider/enabled: true
1833
/jwtConfigProvider/key: secret
1934
/jwtConfigProvider/algorithm: HS256

0 commit comments

Comments
 (0)