Skip to content

Commit dbfed2f

Browse files
committed
Adding client credentials
1 parent 6f9f6f8 commit dbfed2f

File tree

5 files changed

+131
-36
lines changed

5 files changed

+131
-36
lines changed

README.md

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,69 @@
11
# oidc-auth-client
22

3+
[![Build and Publish](https://github.com/norwegian-geotechnical-institute/oidc-auth-client/actions/workflows/release.yaml/badge.svg)](https://github.com/norwegian-geotechnical-institute/oidc-auth-client/actions/workflows/release.yaml)
4+
35
## Installing
46

57
`pip install oidc-auth-client`
68

79
## Example usage
810

11+
### Authorization Code Flow
12+
13+
Use this flow when your application needs to **authenticate a real user**.
14+
15+
It will:
16+
17+
1. Open the user’s browser
18+
2. Redirect them to your identity provider’s login page
19+
3. Wait for the user to authenticate
20+
4. Receive an authorization code
21+
5. Exchange it for an access token
22+
23+
Best for CLIs, desktop apps, and tools acting **on behalf of a user**.
24+
925
```py
1026
from oidc_auth_client import Config, AuthorizationCode, OidcProvider
1127

12-
access_token = AuthorizationCode(
13-
config=Config(
14-
client_id="<your-client-id>",
15-
oidc_provider=OidcProvider(
16-
openid_configuration_url="<auth-provider-url>/.well-known/openid-configuration"
17-
),
18-
token_cache=TokenCache(),
28+
config = Config(
29+
client_id="<your-client-id>",
30+
oidc_provider=OidcProvider(
31+
openid_configuration_url="<auth-provider-url>/.well-known/openid-configuration"
1932
)
20-
).get_token()
33+
)
34+
35+
# Sign in with the identity provider in the opened browser!
36+
access_token = AuthorizationCode(config=config).get_token()
37+
```
38+
39+
### Client Credentials Flow
40+
41+
Use this flow when your application needs to authenticate **as itself, without user interaction**.
42+
The client exchanges its own `client_id` and `client_secret` directly for an access token.
43+
44+
Ideal for:
45+
46+
- Backend services
47+
- Server-to-server APIs
48+
- Cron jobs
49+
- Automated tasks
50+
51+
```py
52+
import os
53+
from oidc_auth_client import ClientCredentials, OidcProvider, TokenCache
54+
from oidc_auth_client.client_credentials import ClientCredentialsConfig
55+
56+
config = ClientCredentialsConfig(
57+
client_id=CLIENT_ID,
58+
client_secret=CLIENT_SECRET,
59+
oidc_provider=OidcProvider(openid_configuration_url=OPENID_CONFIGURATION_URL),
60+
)
61+
62+
access_token = ClientCredentials(config=config).get_token()
2163
```
2264

65+
#### Token caching between runs
66+
2367
To allow the user to cache the token between usages, configure with a `TokenCache`. Will store the token in plaintext on the users system.
2468

2569
```py
@@ -31,4 +75,4 @@ config = Config(
3175
)
3276
```
3377

34-
see some example usage in the [examples folder](./examples/).
78+
see some example usage in the [examples folder](https://github.com/norwegian-geotechnical-institute/oidc-auth-client/tree/main/examples).

examples/client_credentials.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os
2+
from oidc_auth_client import ClientCredentials, OidcProvider, TokenCache
3+
from oidc_auth_client.client_credentials import ClientCredentialsConfig
4+
5+
CLIENT_ID = os.getenv("CLIENT_ID")
6+
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
7+
OPENID_CONFIGURATION_URL = os.getenv("OPENID_CONFIGURATION_URL")
8+
9+
if not CLIENT_ID or not CLIENT_SECRET or not OPENID_CONFIGURATION_URL:
10+
raise Exception(
11+
"Required env vars CLIENT_ID, CLIENT_SECRET and OPENID_CONFIGURATION_URL not supplied"
12+
)
13+
14+
config = ClientCredentialsConfig(
15+
client_id=CLIENT_ID,
16+
client_secret=CLIENT_SECRET,
17+
oidc_provider=OidcProvider(openid_configuration_url=OPENID_CONFIGURATION_URL),
18+
token_cache=TokenCache(),
19+
)
20+
21+
print(ClientCredentials(config=config).get_token())
Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,38 @@
1-
from oidc_auth_client.authentication_flow import AuthenticationFlow
2-
from oidc_auth_client.oidc import OidcProvider
3-
from oidc_auth_client.token_cache import TokenCache
1+
from dataclasses import dataclass
2+
from oidc_auth_client.authentication_flow import AuthenticationFlow, Config
3+
4+
5+
@dataclass(frozen=True, kw_only=True)
6+
class ClientCredentialsConfig(Config):
7+
client_secret: str
48

59

610
class ClientCredentials(AuthenticationFlow):
711
def __init__(
812
self,
9-
client_id: str,
10-
client_secret: str,
11-
tokens_url: str,
12-
timeout: int = 10, # Seconds
13-
app_name: str = "geohub_auth",
14-
loglevel: str = "INFO",
15-
oidc_provider: OidcProvider | None = None,
16-
token_cache: TokenCache | None = None,
13+
config: ClientCredentialsConfig,
1714
) -> None:
18-
self._client_id = client_id
19-
self._client_secret = client_secret
20-
self._tokens_url = tokens_url
21-
self._timeout = timeout
22-
23-
super().__init__(app_name, loglevel, oidc_provider, token_cache)
15+
self._client_secret = config.client_secret
16+
super().__init__(config)
2417

2518
def get_token(self) -> str:
26-
token: str | None = self._token_cache.load_cached_token()
27-
if token is not None:
28-
return token
19+
if self._token_cache is not None:
20+
token: str | None = self._token_cache.load_cached_token()
21+
if token is not None:
22+
return token
2923

3024
tokens = self._oidc_provider.tokens(
31-
self._tokens_url,
3225
data={
33-
"client_id": self._client_id,
34-
"client_secret": self._client_secret,
3526
"grant_type": "client_credentials",
27+
"client_id": self._config.client_id,
28+
"client_secret": self._client_secret,
3629
},
3730
)
31+
3832
self._logger.debug("tokens successfully obtained")
3933

40-
self._token_cache.cache_tokens(tokens)
34+
if self._token_cache is not None:
35+
self._token_cache.cache_tokens(tokens)
36+
4137
token = tokens["access_token"]
4238
return token

src/oidc_auth_client/token_cache.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,16 @@ def cache_tokens(self, tokens: TokenResponseBody):
114114
cache_file_path = self._get_cache_file()
115115
self.logger.debug(f"Caching tokens at file path: {cache_file_path}")
116116

117-
tokens["expires_at"] = time.time() + tokens["expires_in"]
117+
_tokens = {
118+
"access_token": tokens["access_token"],
119+
"expires_at": time.time() + tokens["expires_in"],
120+
}
118121

119122
with open(cache_file_path, "w") as f:
120-
json.dump(tokens, f)
123+
json.dump(_tokens, f)
121124

122125
self.logger.debug(
123-
f"Successfully cached tokens. Expires at {ts(tokens['expires_at'])}"
126+
f"Successfully cached tokens. Expires at {ts(_tokens['expires_at'])}"
124127
)
125128

126129
def load_cached_token(self) -> str | None:

tests/test_client_credentials.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from oidc_auth_client import ClientCredentials
2+
from oidc_auth_client.client_credentials import ClientCredentialsConfig
3+
from oidc_auth_client.oidc import TokenResponseBody
4+
from tests.mocks import MockOIDC
5+
6+
7+
def test_client_credentials_happypath():
8+
expected_client_id = "EXPECTED_CLIENT_ID"
9+
expected_client_secret = "EXPECTED_CLIENT_SECRET"
10+
expected_access_token = "MOCK_ACCESS_TOKEN"
11+
12+
def tokens(data: dict) -> TokenResponseBody:
13+
actual_client_id = data["client_id"]
14+
actual_client_secret = data["client_secret"]
15+
16+
assert expected_client_id == actual_client_id
17+
assert expected_client_secret == actual_client_secret
18+
19+
return {"access_token": expected_access_token, "expires_in": 600}
20+
21+
actual_access_token = ClientCredentials(
22+
config=ClientCredentialsConfig(
23+
client_id=expected_client_id,
24+
client_secret=expected_client_secret,
25+
oidc_provider=MockOIDC(tokens=tokens),
26+
),
27+
).get_token()
28+
29+
assert expected_access_token == actual_access_token
30+
31+
raise Exception(actual_access_token)

0 commit comments

Comments
 (0)