Metadata-Version: 2.4
Name: tsgauth
Version: 0.12.0.dev0
Summary: The TSG authentication library for use with the CERN SSO (OIDC based) service
Project-URL: Homepage, https://gitlab.cern.ch/cms-tsg/common/tsgauth
Project-URL: Bug Tracker, https://gitlab.cern.ch/cms-tsg/common/tsgauth/issues
Author-email: Sam Harper <cmstsg@cern.ch>
License-File: LICENSE.txt
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Requires-Dist: authlib<1.8,>=1.6
Requires-Dist: requests
Provides-Extra: dev
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: pycryptodome; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: uvicorn; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: aiocache; extra == 'fastapi'
Requires-Dist: fastapi[standard]; extra == 'fastapi'
Requires-Dist: itsdangerous; extra == 'fastapi'
Requires-Dist: pydantic-settings; extra == 'fastapi'
Requires-Dist: redis; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=2.0; extra == 'flask'
Requires-Dist: redis; extra == 'flask'
Provides-Extra: kerb
Requires-Dist: bs4; extra == 'kerb'
Requires-Dist: inputimeout; extra == 'kerb'
Requires-Dist: requests-gssapi; extra == 'kerb'
Description-Content-Type: text/markdown

# tsgauth

A collection of python base CERN SSO based authentication and authorisation tools used by the TSG. It provides methods for both users trying to access SSO protected sites in python and for sites to add SSO protection to their endpoints. It is minimal and tries to stay out of the way of the user as much as possible.

The current version is 0.12.0

It is pip installable by 
```bash
pip3 install tsgauth==0.12.0
pip3 install tsgauth[flask]==0.12.0  #if you want flask modules
pip3 install tsgauth[fastapi]==0.12.0 #if you want fastapi modules
```

We currently support python 3.9+ as that is what the majority of CMSSW releases setup but this is on borrowed time. We will try to retain functionality of the client side modules for python 3.9 as long as
possible but server side modules may require a more recent version of python in the near future. 

We pass tests in 3.9, 3.10, 3.11, 3.12 but fail 3.14 due to the fork changes. TSG servers
run on 3.11 currently although we expect to move to 3.12 shortly. 

Version policy: The major version number will be incremented for any breaking changes. The minor version number will be incremented for any new features. The patch version number will be incremented for any bug fixes. The package is currently in development and will be so till it hits version 1.0.0, until then these rules will be a bit looser.

Currently **the session auth capabilities are still under development** and may change in future versions, though they are no longer considered experimental. If there is a security hole somewhere in the package, it's likely in this feature. Session auth is only supported when the frontend and the api are on the same site, see the origins and sites section.

It is intended that users use keyword arguments when passing into the function as the order of the arguments may change in minor versions with the exception of client_id which is always first. Only public modules, methods and members (ie do not start with _) are considered part of the API and thus subject to the version policy. A module whose name starts with an underscore, `tsgauth._redirects` for example, is an internal detail of the package however public the names inside it look, and may change or disappear in any version. Changes to the internals will not be considered breaking changes but will be considered enough to bump the minor version number.

Support requests can be raised on the [gitlab issue tracker](https://gitlab.cern.ch/cms-tsg-fog/tsgauth/-/issues) or by contacting Sam Harper on mattermost (preferred). If you dont hear from Sam after a few days, please ping him again as he may have missed your message.

This package is provided as is, with no warranty of any kind. While every effort is made to ensure its correctness, it is intended to be used as a base for your own applications and you should review the code to ensure it meets your security requirements. We welcome any feedback, particularly if security related. 

## Security Warning

To use this package securely there are two things you need to do:

1. if you use the option to persist sessions ensure that the resulting authentication files stored in ~/.tsgauth are not compromised. Whoever has these files has the privileges they represent. They are created to be only read/writable by the user but if you copy them about, you need to ensure they are protected. **this option is set by default for tokens** 
1. if you use pip, **always specify a version**, ie `pip3 install tsgauth==0.12.0` not `pip3 install tsgauth` to prevent a [supply chain attack](https://en.wikipedia.org/wiki/Supply_chain_attack). This is a good idea for packages in general but is critical here. Otherwise you are trusting that a malicious actor has not compromised my pypi account and uploaded a malicious version of the package which could either intercept OTPs or send the resulting authentication files to a remote server. It would not be possible for them to access your password, just the auth session cookie/ access token. Note, it is not possible for anybody to upload new code as an existing version to pypi, ie `pip3 install tsgauth==0.12.0` will always install the same code.

For securing APIs, it should be secure when passing in tokens as the verification is simple. When enabling session auth, more possible attack vectors are opened up. Currently the session auth is known to be susceptible to a CSRF attack where an attacker can trick a user into authenticating as the attacker rather than as themselves. With the way the CERN SSO operates the attacker would still need access rights to the application, so for a typical application open to ATLAS or CMS members the practical gain to them is very limited if it exists at all. 

Particular attention should be paid to session auth as it uses cookies. Left to itself a browser attaches a site's cookies to every request made to that site, whoever caused the request. So evilsite could make a request to yoursecuredsite, it would carry your session cookie, and you would never notice.

What stops that is the `SameSite` attribute. Setting it to `Lax` or `Strict` tells the browser not to send the cookie on cross-site requests, and `Lax` is the default in tsgauth. Note this is about the *site*, not the host: a frontend and an api on two different hosts under `cern.ch` are the same site, so this default does not stand in the way of splitting them. See the origins and sites section for what does.

The alternative, `SameSite=None` with `Secure`, does allow the cookie to be sent across sites but leaves you relying entirely on CORS to tell wanted requests from unwanted ones. We do not recommend or support it, both for that reason and because Safari and Firefox will not send such a cookie anyway. Again see the origins and sites section.

The [CORSMiddleware](https://fastapi.tiangolo.com/tutorial/cors/) is a popular option in fastapi to handle CORS. Note that `allow_origins=["https://*.yourdomain"]` will not work as expected as the wildcard will not be interpreted as you expect. You will need to explicitly list all the origins you wish to allow or use `allow_origin_regex` to specify a regex which matches the origins you wish to allow. See the [docs for more details](https://fastapi.tiangolo.com/tutorial/cors/#use-corsmiddleware).


## Quick start

### How to Access SSO CERN sites in python using TSGAuth

This is a minimal explanation for the impatient who just want to access a SSO protected website using python. For a more detailed explanation, please see the rest of this guide. TSGAuth is designed assuming you are using the `requests` module but exposes methods which will work with any module which can make http requests assuming you can pass cookies and headers to it.


There are different ways to access SSO protected sites on the cmdline, there are a series of classes in tsgauth.oidcauth for various types of authorisation and authentication mechanisms. They are all designed such that

```python
auth = tsgauth.oidcauth.<AUTHCLASS>()
r = requests.get(url,**auth.authparams()) #note depending on the auth class, it may override your headers, thus you need to 
                                          #pass in any headers you want in the authparams call, eg
                                          #**auth.authparams(headers={"Accept":"application/json"})
```
will work for all of them.

The only thing the user needs to do is select the correct class. To do this you need to know if the website (aka protected resource) you which to access is using session/cookie or token based authorisation. 

If it's cookie based, you will need to use a SessionAuth derived class of which the only one is `tsgauth.oidcauth.KerbSessionAuth()` which uses kerberos to authenticate. If it is token based, you need a TokenAuth class, of which there are three, `tsgauth.oidcauth.KerbAuth()`, `tsgauth.oidcauth.ClientAuth()` and `tsgauth.oidcauth.DeviceAuth()` depending on how you wish to authenticate.  You will also need to know the client id of the application you wish to access as well as its redirect_uri. If it is a confidential client, you will also need the client secret. 

Most users will want `tsgauth.oidcauth.KerbAuth()` which uses kerberos to authenticate. Unlike the CERN sso-get-cookie and sso-get-token, tsgauth supports accounts with 2FA enabled. 

**note: for 2FA, only OTP codes are supported**, you have to have a OTP option enabled on your account for this to work (eg the numbers you type in from your phone). It does not have to be the default though, the code will look for the OTP op. This is because we cant transmit the 2FA challenge to the user in a way they can respond to it on the command line in a remote server. If you have a good idea how to do this, please let us know.

examples using kerberos 

```python
auth = tsgauth.oidcauth.KerbAuth("cms-tsg-frontend-client")
r = requests.get("https://hltsupervisor.app.cern.ch/api/v0/thresholds",**auth.authparams())
```

```python
auth = tsgauth.oidcauth.KerbSessionAuth()
r = requests.get("https://twiki.cern.ch/twiki/bin/view/CMS/TriggerStudies?raw=text",**auth.authparams())
```

examples using device auth

```python
auth = tsgauth.oidcauth.DeviceAuth("cms-tsg-frontend-client")
r = requests.get("https://hltsupervisor.app.cern.ch/api/v0/thresholds",**auth.authparams())
```

Some sites have a confidential client. We dont have the secret so we cant get those tokens. Some of these have a 
public client we can get a token for an exchange it for the confidential client token.  An example is oms

```python
auth = tsgauth.oidcauth.DeviceAuth("cmsoms-prod-public",target_client_id="cmsoms-prod")
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
url = f"https://cmsoms.cern.ch/agg/api/v1/l1triggerrates?page[offset]=0&page[limit]=20&filter[run_number][EQ]=402097&include=meta,presentation_timestamp&group[granularity]=run"
r = requests.get(url,**auth.authparams(),verify=False)
```

Note here the `verify=False` and `requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)` is needed because the oms server has a self signed certificate and may not be trusted by your system. Ideally you should add the oms certificate to your trusted certificates, this is just an example to quickly get it working.

Finally, you can log in with client auth, which is a like an extremely limited account with minimial permissions. Here the user is the client id not you and the client only has the permissions the application has been granted. This is best 
for automated scripts so you dont have to supply user credentials. The owner of the application you want to access will 
have to grant your client permissions to access their application. 

```python
auth = tsgauth.oidcauth.ClientAuth("your client id",client_secret="your client secret")
r = requests.get("https://hltsupervisor.app.cern.ch/api/v0/thresholds",**auth.authparams())
```

As a final heads up, the AuthClasses can persist cookies and tokens to disk so you dont need to reauthenticate every time. This is true by default for KerbSessionAuth, DeviceAuth classes. The directory should only be readable by the user and is `~/.tsgauth` by default but you can override it by setting the `TSGAUTH_AUTHDIR` environmental variable.  **These files should be protected as they grant access as you to the given application.** Note, it is not an error for the application to fail to read/write to this directory, it will continue as is but log a warning. The logging level is controlled by the `TSGAUTH_LOGLEVEL` environmental variable and defaults to `ERROR`. The writing of the authentication files is controlled by the parameter `use_auth_file` passed in the constructor of the auth class. For convenience you can also force enabling / disabling of this feature globally by setting the environmental variables `TSGAUTH_FORCE_USE_AUTHFILE` / `TSGAUTH_FORCE_DONT_USE_AUTHFILE` to 1. 

A summary of the environmental variables is as follows:
 * TSGAUTH_LOGLEVEL : logging level ("NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
 * TSGAUTH_AUTHDIR : directory where the auth files are written if requested to be (default: ~/.tsgauth)) 
 * TSGAUTH_FORCE_USE_AUTHFILE : forces the authfile to be written/used (set to 1 to do this)
 * TSGAUTH_FORCE_DONT_USE_AUTHFILE : forces the authfile to not be written/used  (set to 1 do this)
 * TSGAUTH_ALLOWED_ALGS : the allowed algorithms for the token, unset defaults to RS256 (will be updated if CERN SSO changes), can be set to "ALL" to allow all algorithms or a comma separated list of allowed algorithms (the default avoids the risk of signature bypass described in CVE-2016-10555 which 'ALL' would be vulnerable to). Really only important for servers securing resources.
 * TSGAUTH_JWKS_TTL : seconds the json web key set fetched from the SSO is cached for, defaults to 300, set to 0 to fetch it every time. A key the SSO rotates in is picked up straight away whatever this is set to, so this only bounds how long a key the SSO has withdrawn would still be accepted. Really only important for servers securing resources.
 * TSGAUTH_JWKS_MIN_REFETCH_INTERVAL : seconds, defaults to 60, the shortest time between two of those early refetches for a given key set. A token signed by a key we do not have is what tells us the SSO has rotated its keys and sends us back to fetch them, this limits how often a caller we have not authenticated yet can make us do that. It runs from the last such refetch and not from the last ordinary fetch, so the first token signed by a newly rotated in key always sends us back to the SSO. Really only important for servers securing resources.
 * TSGAUTH_JWKS_TIMEOUT : seconds, defaults to 10, minimum 1, how long to wait on the SSO when fetching the key set. Raise it if you reach the SSO through a slow proxy. There is deliberately no way to wait forever, an SSO which accepts the connection and then never answers would otherwise hang the caller, and under fastapi the event loop with it. Really only important for servers securing resources.

#### Determining if a resource expects Session or Cookie based authorisation

The easiest way to find out how to service expects you to authenticate is ask the owner or review their documentation. As this is not always possible, you can open it up in a web browser and see how the browser is making requests. 

If you see the the requests to the protected api  have a header {"Authorization", "Bearer <long string>}" it is token based. You should also see the browser requesting said token, with something like:

```
https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/auth?client_id=cms-tsg-frontend-client&redirect_uri=https%3A%2F%2Fhltsupervisor.app.cern.ch%2F&state=8dbacbe6-e06e-4fb9-8699-eb87c136195a&response_mode=fragment&response_type=code&scope=openid&nonce=3d5ff976-fd51-43aa-8b0d-3a72c2782b20
```
this gives your client_id (cms-tsg-frontend-client) and a valid redirect_uri (https://hltsupervisor.app.cern.ch/) which you can use to request a token.

If you dont see anything like this, its session based (you'll probably see a cookie auth session or similar). Session Cookie auth is mainly done public services using confidential clients. The client (say an apache server which is interacting with the resource server on your behalf) handles the token exchange and the user never sees the token. It will instead issue you a cookie so identify you for the authentication session. 

### Securing APIs with TSGAuth

Currently we support FastAPI and Flask. If you have a choice, we would recommend you using FastAPI as it is a more modern framework. The FastAPI implementation has more features and is less error prone and is the primary development focus. However we still intend to support Flask for the foreseeable future.

Examples of FastAPI and Flask implementations can be found in the examples directory.

### Securing FastAPI sites

If you wish to secure an endpoint on your fast api system, you just need to make your endpoint depend on tsgauth.fastapi.JWTBearerClaims. This will validate the user claims (unless validate_token=False) and make them available to your endpoint. 

```python
from tsgauth.fastapi import JWTBearerClaims
@app.get("/api/v0/secure")
def secure_endpoint(claims = Depends(JWTBearerClaims())):
   return {"claims" : claims}
```
This will validate the user claims with an audience of the client id specified in the OIDC_CLIENT_ID environmental variable and make the claims available to your endpoint. If you have no further need of the claims info, you can put the depends in the decorator. 

However a better way to ensure you do not forget to add the claims dependency to secure an endpoint is to add it to the app as a global dependency. This will ensure that all endpoints are protected by default. Even better would be to add it to a router which you then include in your app so you can choose which endpoints are protected. In our opinion, best practice is to have all your secure endpoints in a router in a separate file (s) which you then include in your app. This way you can not forget to add the dependency to your secure endpoints. When doing this rather than accessing the claims directly, you access them via the `request.state` object. 

An example

```python
#all routes declared on this router will be protected by default
#they will also have the tag "secure" for convenience
secure_route = APIRouter(dependencies=[Depends(JWTBearerClaims())],tags=["secure"])
@secure_route.get("/api/v0/secure")
def secure_endpoint(request: Request):
    return {"claims" : request.state.claims}
#adds the router to the file
app.include_router(secure_route)
```


You can see an example of this in the examples/fastapi_server.py file

#### Session Auth

In the base setup, the fast api server relies on a client to pass in the token. However this is awkward when you 
wish the user to directly access the api endpoint in the browser. In this case, you can set the OIDC_SESSION_AUTH_ALLOWED environmental variable to True. This will cause the server to request a token on behalf of the client if one is not passed in and start an internal authentication session. 

Beyond setting OIDC_SESSION_AUTH_ALLOWED to True, you will also need add the following to your fastapi configuration

```python
import tsgauth.fastapi

app = FastAPI()
tsgauth.fastapi.setup_app(app,secret_key=your_secret_key)
```

`setup_app` adds the SessionMiddleware for you, so you should not add it yourself as well. If you want to control the cookie attributes, pass `same_site`, `https_only` or `domain` to `setup_app` or set the corresponding environmental variables, see the origins and sites section below.

Remember that the secret key should be a long random string that is not shared with anybody. This is used to sign the session data and if anybody has this key, they can fake the session data and bypass the authentication.

This sets a session cookie to handle the auth session for the application. It then request a token from the CERN SSO and store in the auth information received. By default, it stores it in memory but this can be changed to use redis with the following environmental variables set:
```bash
OIDC_SESSION_STORE_TYPE=redis
OIDC_SESSION_STORE_HOST=your redis server host
OIDC_SESSION_STORE_PORT=your redis server port, likely 6379
```
It is also possible to write your own auth session manager to store it how you wish.

If you would rather add the SessionMiddleware yourself, use `setup_app_wo_session_middleware` instead, which adds the auth routes but leaves the middleware to you

```python
import tsgauth.fastapi
from starlette.middleware.sessions import SessionMiddleware

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=your_secret_key, same_site="lax", https_only=True)
tsgauth.fastapi.setup_app_wo_session_middleware(app)
```

Environmental variables for default Session Auth
* OIDC_SESSION_AUTH_ALLOWED : if set to True, the server will request a token on behalf of the client if one is not passed in. Defaults to False 
  * this is how you enable session auth, by default it is not enabled
* OIDC_SESSION_LIFETIME : the lifetime of the session claims in seconds (default 28800 seconds, 8 hours)
  * if you set this longer than 12 hours, this will start to request an offline token. The idle time of an offline token is currently 30 days but can be renewed indefinitely. 
* OIDC_SESSION_STORE_TYPE: can be "memory" or "redis" (default "memory")
  * the type of store to use for the session data. We recommend redis for production as this will persist the session data across server restarts and also across multiple instances of the server
* OIDC_SESSION_STORE_HOST: the address of the redis server (default "localhost")
* OIDC_SESSION_STORE_PORT: the port of the redis server (default 6379)
* OIDC_ALLOWED_REDIRECT_ORIGINS: comma separated list of origins the auth flow may send the browser back to (default empty)
  * see the origins and sites section below, the origin serving the api is always allowed so same origin setups do not need this
* OIDC_SESSION_COOKIE_SAMESITE: the SameSite attribute of the session cookie, "lax", "strict" or "none" (default "lax")
* OIDC_SESSION_COOKIE_SECURE: the Secure attribute of the session cookie (default False)
* OIDC_SESSION_COOKIE_DOMAIN: the Domain attribute of the session cookie (default unset, ie the host which set it)

#### Origins and sites

Where your frontend is served from decides what you need to configure. The browser asks two separate questions about it and it is very easy to lose a day to confusing them, so these terms are used precisely from here on.

* **origin** : the scheme, host and port together, eg `https://api.app.cern.ch`. Change any one of the three and it is a different origin, so `https://api.app.cern.ch` and `https://api.app.cern.ch:8443` are different origins.
* **site** : the scheme plus the registrable domain, which is the [public suffix](https://publicsuffix.org/) of the host plus the one label in front of it. For `frontend.app.cern.ch` the public suffix is `ch`, as `app.cern.ch` is not itself on the list, so the registrable domain is `cern.ch`. `api.app.cern.ch` gives the same answer, so both are the site `https://cern.ch`.
* **host** : just the hostname. The browser never decides anything from the host on its own, which is exactly why two different hosts can still be the same site.

The scheme counts in both, so `http://` and `https://` on the same host are neither the same origin nor the same site. `localhost` and an ip address are each their own site.

The two questions are independent of each other:

| question | answered by |
| --- | --- |
| do I need CORS, credentials and the redirect allow list? | the **origin**. A different origin means yes |
| do session cookies work at all? | the **site**. A different site means no |

Which leaves three shapes a deployment can take. A cross-site setup is always also cross-origin, so there is no fourth.

| shape | example | session auth |
| --- | --- | --- |
| **same-origin** | the api serves the frontend itself | works, nothing to configure |
| **cross-origin, same-site** | `https://frontend.app.cern.ch` and `https://api.app.cern.ch` | works, see below |
| **cross-site** | `http://localhost:8080` and `https://api.app.cern.ch` | not supported, use a token instead |

##### cross-origin, same-site

This works and is the normal way to split a frontend from its api. You need three things:

1. the frontend origin in the CORS configuration of your api, with `allow_credentials=True`. See the security warning section above
2. the frontend to actually send its credentials, `withCredentials: true` in axios or `credentials: "include"` in fetch. Without this the browser sends no cookies at all on a cross-origin request and it looks exactly like the cookie being blocked
3. the frontend origin in `OIDC_ALLOWED_REDIRECT_ORIGINS`, see the next section

Being on the same site is **not** enough on its own. All three are needed for any frontend which is not on the same origin as the api.

##### cross-site

We do not support session auth across sites, because there is no way to make it work in every browser. The session cookie is a third party cookie in this situation: Safari blocks it outright and Firefox partitions it by top level site. Chrome still sends it today, but has repeatedly signalled that it may stop doing so in the future, so a site relying on it could break at any time.

Setting `OIDC_SESSION_COOKIE_SAMESITE=none` with `OIDC_SESSION_COOKIE_SECURE=True` is what would be required and the settings exist, but it buys you Chrome only. If you need a genuinely cross-site frontend, have it obtain a token itself with something like keycloak-js and pass that to the api as a bearer token, which is what `JWTBearerClaims` handles by default and which none of this section applies to.

The case which catches people out is a frontend on `http://localhost:8080` talking to an api on `https://something.app.cern.ch`. That is cross-site, while the deployed version of the same pair is not, so it fails only in development. Note that two ports on localhost, say `http://localhost:8080` and `http://localhost:5000`, are the same site and cookies are not scoped by port either, so running the api locally as well is a straightforward way to exercise the session auth workflow.

#### The redirect allow list

The auth flow has to send the browser back to where it came from once the login is done, and where that is has to come from the request. Anything reflected from a request into a redirect is an open redirect unless it is checked, so the url is validated against

* the origin serving the api, which is always allowed and needs no configuration
* relative urls, which are on that same origin by definition
* anything listed in `OIDC_ALLOWED_REDIRECT_ORIGINS`

```bash
OIDC_ALLOWED_REDIRECT_ORIGINS=https://frontend.app.cern.ch,http://localhost:8080
```

Matching is on the origin only, so the scheme, host and port must all match exactly; the path is not considered. Anything else is refused with a 400.

So any deployment where the frontend is not on the same origin as the api **must** list the frontend origin here. This applies to cross-origin, same-site deployments too, being on the same site is not enough. Same-origin deployments need nothing.

If you miss it, the login itself succeeds and the failure appears as a 400 on the way back from the SSO, which does not obviously point at a missing setting.

##### Running behind a proxy

The origin the api believes it serves is taken from the connection it sees, not from the one the browser made, so a proxy which terminates TLS has to be trusted before the api knows it is reached on `https`. Uvicorn takes the scheme from `X-Forwarded-Proto` only for clients in `FORWARDED_ALLOW_IPS`, which defaults to `127.0.0.1`. That covers a proxy on the same machine and nothing else, so on kubernetes or openshift, where the ingress is a separate pod, the default leaves the api thinking it serves `http://your.app.cern.ch` while every user reaches it on `https://`. As the scheme is part of the origin, the frontend then fails the allow list on a url which looks perfectly correct.

```bash
FORWARDED_ALLOW_IPS='*'  #or the address range of your proxy, which is tighter
```

The `redirect_uri` the api hands to the SSO is built from this same information, so getting it wrong often announces itself as the login failing outright. Do not rely on that as a safety net though: if the client registration accepts an `http` redirect uri, which a loose wildcard in it will do, and the proxy upgrades `http` to `https`, the code still reaches the callback and the login succeeds while every url the api builds is quietly wrong. The 400 tells you which origin the api thinks it serves, so compare that against the url in your browser.

#### Custom SessionAuth store

Currently the session auth is handled by SessionAuthTokenStore which is a simple memory based store. A user is assigned a unique session id which is saved in the session cookie and used to look up the auth information in the store. 

Alternative stores are supported and can be implemented by creating a class which inherits from SessionAuthBase and implements the following methods:

  * claims : returns the claims the user has. If it wishes to trigger an token request, it must raise a MissingAuthException which will start the process to request a token from the SSO
  * store  : stores the claims for the user, raises SessionAuthBase.AuthResponseException if the store fails. Additionally if
  it wants an offline token but the current token is not one, it should raise a SessionAuthBase.TokenNotOfflineException
  * clear  : clears all auth data from the store and cookie but does not log the user out of the SSO. A parameter "deep" is passed defaulting to False, what deep means is up to the implementation. In the reference, its used to clear offline tokens
  * token_request_allowed : returns True if the application is allowed to request a token, False otherwise. This is mainly used to stop infinite loops of the application requesting a token from the SSO and the SSO redirecting the application to request a token from the SSO.
  * auth_attempt : registers that an auth attempt is occurring

In the SessionAuthTokenStore, it uses a counter to determine how many auth attempts have happened for the request and stops it after 5 to stop infinite loops. auth_attempt is used to increment this counter.

Note nothing says your SessionAuth class actually requires a session cookie, you have complete freedom to implement it how you wish. Nor does it have to request a token, you can even just always return the same claims if you wish which could be useful for testing.

In examples/fastapi_server.py there is an example of such a custom store.

To use your custom store, you need to override the dependency `get_auth_store` in the fastapi module. 
```python
# Register a custom auth_store
def custom_auth_store() -> tsgauth.fastapi.SessionAuthBase:
    return CustomSessionAuth()

app.dependency_overrides[tsgauth.fastapi.get_auth_store] = custom_auth_store
```


#### Configuration 

The auth settings are configured from environmental variables. The following are available:

   * OIDC_CLIENT_ID : the client id of the application you wish to access (required)
   * OIDC_CLIENT_SECRET: the client secret of the application you wish to access (only required for confidential clients, not set for public clients and only makes sense if OIDC_SESSION_AUTH_ALLOWED is set to True)
   * OIDC_SESSION_AUTH_ALLOWED : if set to True, if the token is not passed to a token requiring endpoint, the application will self request a token and manages it itself.  Defaults to `False` when means a token will always have to be passed into a token requiring endpoint by the calling client. **Important If you set this to True, YOU MUST pass a `secret_key` to the function `setup_app`. The `secret_key` is a random long secure string that is not shared with anybody.**  This is used to sign the session token / token info in the session data and if anybody has this key, they can fake this data and essentially bypass the authentication. As it's a secret, it should not be in the code but set via an environmental variable or config file. 
   * OIDC_SESSION_LIFETIME : how long an auth session lasts in seconds
   * OIDC_SESSION_STORE_TYPE : the type of store to use for the session data. We recommend `redis` for production as this will persist the session data across server restarts and also across multiple instances of the server. Defaults to `memory`
   * OIDC_SESSION_STORE_HOST : the address of the redis server. Defaults to `localhost`
   * OIDC_SESSION_STORE_PORT : the port of the redis server. Defaults to `6379`
   * OIDC_ALLOWED_REDIRECT_ORIGINS : comma separated list of origins the auth flow is allowed to return the browser to, eg `https://frontend.app.cern.ch,http://localhost:8080`. Defaults to empty. The origin serving the api is always allowed, so this is only needed when your frontend is on a different origin to your api. See the origins and sites section
   * OIDC_SESSION_COOKIE_SAMESITE : the `SameSite` attribute of the session cookie, one of `lax`, `strict` or `none`. Defaults to `lax`. Changing it is only relevant to a cross-site setup, which is not supported, see the origins and sites section before you do
   * OIDC_SESSION_COOKIE_SECURE : the `Secure` attribute of the session cookie, ie only send it over HTTPS. Defaults to `False`. Required to be True if the samesite setting is `none`
   * OIDC_SESSION_COOKIE_DOMAIN : the `Domain` attribute of the session cookie. Defaults to unset, which means the cookie goes back only to the host which set it. Widening it sends the cookie to every host under that domain so only set it if you need that

the following are variables depend on the OIDC provider you are using. The defaults are set up for the CERN SSO so for users of the CERN SSO (most if not all of our users) you do not need to set these. They are
   * OIDC_ISSUER : the OIDC issuer, defaults to "https://auth.cern.ch/auth/realms/cern"
   * OIDC_JWKS_URI : the OIDC JWKS URI, defaults to "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/certs"
   * OIDC_AUTH_URI : the OIDC auth URI, defaults to "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/auth"
   * OIDC_LOGOUT_URI : the OIDC logout URI, defaults to "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/logout"
   * OIDC_TOKEN_URI : the OIDC token URI, defaults to "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/token"
   
#### JWTBearerClaims options

The JWTBearerClaims class has the following options:
   * validate_token : if set to False, the token will not be validated. This is useful for testing. Obviously should be True if you want any security at all. Defaults to True.
   * require_aud : if True, OIDC_CLIENT_ID must be set and the token's audience must match it. Has no effect if validate_token is False. Defaults to True and you should leave it there, a token is issued for a particular application and accepting one issued for a different application means anybody who can get a token for any client can call your api
   * use_state: if true, it also adds the claims to request.state. Useful for using it as a global dependency. Defaults to True
   * auto_error : you probably dont need to touch this but for completeness this is a pass through to the base BearerAuth class, if true it will raise an exception if the token is not present. However it is best to have this false as the exception returns a 403 code not a 401. If false JWTBearerClaims will raise an exception which will return a 401 code. Defaults to False. 

### Securing Flask sites

In python this was modeled after the flask-oidc package which is completely not recommended but when we started we ended up using due to very inadequate documentation. It requires the following variable to be set in your flask configuration

```python
app.config.update({
   'OIDC_CLIENT_ID' : <your client id>   
}) 
```

The application also allows you set the following parameters to configure it based on which OIDC server you are using. By default it is set up for the CERN SSO so for users of the CERN SSO (most if not all of our users) you do not need to set these. The defaults are:
```python
app.config.update({
   'OIDC_ISSUER' : "https://auth.cern.ch/auth/realms/cern",
   'OIDC_JWKS_URI' : "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/certs",
   'OIDC_AUTH_URI' : "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/auth",
   'OIDC_LOGOUT_URI' : "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/logout",
   'OIDC_TOKEN_URI' : "https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/token",
})
```
If you use a different OIDC provider, you will need to set these to the correct values appropriate for your provider.


The following parameters are optional:
`OIDC_SESSION_AUTH_ALLOWED` : if set to True, if the token is not passed to a token requiring endpoint application will self request a token and pass it back to the client if public token or token_info if its a private token.  Defaults to `False` when means a token will always have to be passed into a token requiring endpoint by the calling client. **Important If you set this to True, YOU MUST SET the flask secret key (SECRET_KEY) to a random long secure string that is not shared with anybody.** 

`OIDC_CLIENT_SECRET`: required if the token is a private token and thus requires a secret to obtain. This is only needed if OIDC_SESSION_AUTH_ALLOWED is set to True. There is no default value.

`OIDC_SESSION_LIFETIME`: this is the max time in seconds that an auth session is valid. Defaults to 28800 seconds (8 hours). This only applies to private tokens, public tokens will be send to client and the expiry is managed in the normal way. OIDC_SESSION_AUTH_ALLOWED must be set to True for this to have any effect. Note the flask implementation does not yet support offline tokens and thus the max possible lifetime is 12 hours which in practise will be shorter as it will expire when your CERN SSO session ends. Currently there are no plans to support longer sessions in flask but we can discuss if needed. The FastAPI implementation does support offline tokens and thus can have a longer session lifetime and you may wish to consider using that instead. If OIDC_SESSION_MANAGE_COOKIE and OIDC_SESSION_COOKIE_PERMANENT are both set to True, this will also be the lifetime of the session cookie.

`OIDC_SESSION_STORE_TYPE`: the type of store to use for the session data. We recommend `redis` for production as this will persist the session data across server restarts and also across multiple instances of the server. Defaults to `simplemem`

`OIDC_SESSION_STORE_HOST`: the address of the redis server. Defaults to `service/redis`. Only used if using redis as the store.

`OIDC_SESSION_STORE_PORT`: the port of the redis server. Defaults to `6379`. Only used if using redis as the store.

`OIDC_SESSION_MANAGE_COOKIE` : where the tsgauth package manages the session cookie, setting the samesite, the lifetime etc.  Defaults to True. If False, you manage it yourself according to the normal flask way of doing it.

`OIDC_SESSION_COOKIE_SAMESITE` : sets the same site of the session cookie. Defaults to "Lax". Overrides SESSION_COOKIE_SAMESITE if OIDC_SESSION_MANAGE_COOKIE is True otherwise has no effect.

`OIDC_SESSION_COOKIE_PERMANENT`: sets the session cookie to permanent. Defaults to True. Only has an effect if OIDC_SESSION_MANAGE_COOKIE is True.

`OIDC_ALLOWED_REDIRECT_ORIGINS`: comma separated list of origins the auth flow is allowed to return the browser to, eg `https://frontend.app.cern.ch,http://localhost:8080`. Defaults to empty. The origin serving the api is always allowed, as are relative urls, so this is only needed when your frontend is on a different origin to your api. Anything else is refused with a 400. The rules are the same ones the FastAPI implementation uses, see the redirect allow list section above.

These other parameters are standard flask parameters which are used to control the session cookie are `SESSION_COOKIE_HTTPONLY` and `SESSION_COOKIE_SECURE` even if OIDC_SESSION_MANAGE_COOKIE is set to True as it does not override them.




Then package can then be used as follows

```python
import tsgauth.flaskoidc as oidc
@application.route('/api/v0/secure', methods=['GET'])
@oidc.accept_token(require_token=True)
def secure_endpoint():
      return jsonify({"claims" : g.oidc_token_info})
```

You can see an example of this in the tests/test_flaskoidc.py file which is run as part of the unit tests

### Using the Token

In the above examples you get the a dictionary with the claims of the token. The two most common use cases are to uniquely identify the user and the roles they have in the application (ie who they are and what they can do). These are in the `sub` and `cern_roles` claims respectively.
  * sub : the subject of the token, ie the user id. This is the unique identifier of the user and typically the cern username but in the case of applications it is `service-account-<applicationname>`, eg for me it is sharper, but if I use the client id and secret of cms-tsg-client to log in it will be `service-account-cms-tsg-client`. 
  * cern_roles:  the roles the user has for this application (ie for the client_id of the token). See below for defining roles. Note this is a duplication of `["resource_access"]["<client_id>"]["roles"]` field. Given you have already validated that this token is for the client_id your application expects, its easier to just access "cern_roles" unless for some reason you are not using the CERN SSO provider

If you wish to know more about the user, you have the following additional claims. None of these are defined for applications (ie login with a client id/secret), only for users, so your application should be able to handle the case where they are not present unless you wish to restrict access to only users and not applications. 
  * name: the users full name
  * given_name: the users given name
  * family_name: the users family name
  * preferred_username: the users preferred username
  * email: the users email address
  * cern_mail_upn: the users cern email identifier
  * cern_upn : the users cern username, the same as sub 
  * cern_email : the users email address
  * cern_person_id : the users cern id number
