Skip to content

Commit

Permalink
chore: authentication and authorization implemented full stack and wo…
Browse files Browse the repository at this point in the history
…rks from frontend to both backend API and MS Graph
  • Loading branch information
arnoldknott committed Jan 13, 2024
1 parent 397d728 commit fe0feea
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 126 deletions.
15 changes: 11 additions & 4 deletions backendAPI/src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class Config(BaseSettings):
# Microsoft Azure OAuth 2.0 configuration:
AZURE_TENANT_ID: str = get_variable("AZURE_TENANT_ID")
AZURE_OPENID_CONFIG_URL: str = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/v2.0/.well-known/openid-configuration"
AZURE_ISSUER_URL: str = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/v2.0"
AZURE_CLIENT_ID: str = get_variable("AZURE_CLIENT_ID")
API_SCOPE: str = get_variable("API_SCOPE")

# Client ID of the frontend application registered in Azure AD:
# add "customer" client registrations here!
# APP_REG_CLIENT_ID: str = get_variable("APP_REG_CLIENT_ID")

# Postgres configuration:
# always get those variables from the environment:
Expand Down Expand Up @@ -96,10 +103,10 @@ def build_postgres_url(cls, url: Optional[str], values: ValidationInfo) -> Any:
# Redis configuration:
REDIS_HOST: str = os.getenv("REDIS_HOST")
REDIS_PORT: int = int(os.getenv("REDIS_PORT"))
print("=== REDIS_PORT ===")
print(REDIS_PORT)
print("=== get_variable('REDIS_REDIS_JWKS_DB') ===")
print(get_variable("REDIS_JWKS_DB"))
# print("=== REDIS_PORT ===")
# print(REDIS_PORT)
# print("=== get_variable('REDIS_REDIS_JWKS_DB') ===")
# print(get_variable("REDIS_JWKS_DB"))
REDIS_JWKS_DB: int = int(get_variable("REDIS_JWKS_DB"))
REDIS_PASSWORD: str = get_variable("REDIS_PASSWORD")

Expand Down
83 changes: 60 additions & 23 deletions backendAPI/src/core/security.py → backendAPI/src/core/oauth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
import logging

import httpx
import jwt
from core.cache import redis_jwks_client
from core.config import config
from fastapi import HTTPException, Request
from jwt.algorithms import RSAAlgorithm
Expand All @@ -11,24 +13,29 @@
logger = logging.getLogger(__name__)


def get_jwks():
def get_jwks(no_cache: bool = False):
"""Fetches the JWKs from identity provider"""
logger.info("🔑 Fetching JWKS")
try:
print("=== AZURE_OPENID_CONFIG_URL ===")
print(config.AZURE_OPENID_CONFIG_URL)
oidc_config = httpx.get(config.AZURE_OPENID_CONFIG_URL).json()
# oidc_config.raise_for_status()
print("=== oidc_config ===")
print(oidc_config)
jwks = httpx.get(oidc_config["jwks_uri"]).json()
# print("=== jwks ===")
# print(jwks)
# TBD: add the jwk to the cache!
print("=== jwks needs to be cached! ===")
if not no_cache:
jwks = redis_jwks_client.json().get("jwks")
if jwks:
return json.loads(jwks)
else:
get_jwks(no_cache=True)
else:
oidc_config = httpx.get(config.AZURE_OPENID_CONFIG_URL).json()
if not oidc_config:
raise HTTPException(
status_code=404, detail="Failed to fetch Open ID config."
)
jwks = httpx.get(oidc_config["jwks_uri"]).json()
if not jwks:
raise HTTPException(status_code=404, detail="Failed to fetch JWKS.")
redis_jwks_client.json().set("jwks", ".", json.dumps(jwks))
return jwks
except Exception as err:
logger.error("🔥 Failed to fetch JWKS.")
logger.error("🔥 Failed to get JWKS.")
raise err
# try:
# jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
Expand All @@ -55,32 +62,55 @@ def get_jwks():
# rsa_key = RSAAlgorithm.from_jwk(key)


def validate_token(request: Request):
def validate_token(request: Request, retries: int = 0):
"""Validates the access token sent in the request header"""
# get the token from the header:
# print("=== request.headers ===")
# print(request.headers)
logger.info("🔑 Validating token")
try:
# request.headers.get("Authorization").split("Bearer ")[1]
token = request.headers.get("Authorization").split("Bearer ")[1]
authHeader = request.headers.get("Authorization")
# print("=== authHeader ===")
# print(authHeader)
token = authHeader.split("Bearer ")[1]
if token:
print("=== token exists ===")
jwks = get_jwks()
print("=== jwks ===")
print(jwks)

# Get the key that matches the kid:
kid = jwt.get_unverified_header(token)["kid"]
print("=== kid ===")
print(kid)
rsa_key = {}
for key in jwks["keys"]:
if key["kid"] == kid:
rsa_key = RSAAlgorithm.from_jwk(key)
print("=== rsa_key ===")
print(rsa_key)
# print("=== rsa_key ===")
# print(rsa_key)
# print("=== token ===")
# print(token)
# # print("=== config.APP_REG_CLIENT_ID ===")
# print(config.APP_REG_CLIENT_ID)
# print("=== config.AZURE_ISSUER_URL ===")
# print(config.AZURE_ISSUER_URL)
logger.info("Decoding token")
jwt.decode(
token,
rsa_key,
algorithms=["RS256"],
audience=config.API_SCOPE,
issuer=config.AZURE_ISSUER_URL,
options={
"validate_iss": True,
"validate_aud": True,
"validate_exp": True,
"validate_nbf": True,
"validate_iat": True,
},
)
logger.info("Token decoded successfully")
# print("=== payload ===")
# print(payload)

return True
# Try validating token first with cached keys - if no success, fetch new keys, put them in the cache and try again!
# print(token)
# print("=== get_jwks() ===")
Expand All @@ -98,5 +128,12 @@ def validate_token(request: Request):
# },
# )
except Exception as e:
logger.error(f"🔥 Token validation failed: ${e}")
# only one retry allowed: by now the tokens should be cached!
if retries < 1:
logger.info(
"🔑 Failed to validate token, fetching new JWKS and trying again."
)
get_jwks(no_cache=True)
return validate_token(request, retries + 1)
logger.error(f"🔑 Token validation failed: ${e}")
raise HTTPException(status_code=401, detail="Invalid token")
2 changes: 1 addition & 1 deletion backendAPI/src/routers/api/v1/protected_resource.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import Annotated

from core.security import validate_token
from core.oauth import validate_token
from fastapi import APIRouter, Depends

logger = logging.getLogger(__name__)
Expand Down
2 changes: 2 additions & 0 deletions compose.override.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ services:
- REDIS_SESSION_DB=$REDIS_SESSION_DB
- REDIS_PASSWORD=$REDIS_PASSWORD
- AZURE_TENANT_ID=$AZURE_TENANT_ID
- AZURE_CLIENT_ID=$AZURE_CLIENT_ID
- API_SCOPE=$API_SCOPE
networks:
- test_network
ports:
Expand Down
8 changes: 8 additions & 0 deletions frontend_svelte/src/lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { SecretClient } from "@azure/keyvault-secrets";
export default class AppConfig{
private static instance: AppConfig;
public api_scope: string;
public api_scope_default: string;
public app_reg_client_id: string;
public app_client_secret: string;
public az_authority: string;
public az_logout_uri: string;
public backend_host: string;
public backend_origin: string;
public keyvault_health?: string;
Expand All @@ -24,9 +26,11 @@ export default class AppConfig{

private constructor(){
this.api_scope = '';
this.api_scope_default = '';
this.app_reg_client_id = '';
this.app_client_secret = '';
this.az_authority = '';
this.az_logout_uri = '';
this.backend_host = process.env.BACKEND_HOST,
this.backend_origin = `http://${process.env.BACKEND_HOST}:80`,
this.keyvault_health = '';
Expand Down Expand Up @@ -95,7 +99,9 @@ export default class AppConfig{
this.app_reg_client_id = appRegClientId?.value || '';
this.app_client_secret = appClientSecret?.value || '';
this.api_scope = apiScope?.value || '';
this.api_scope_default = `api://${apiScope?.value}/.default`;
this.az_authority = `https://login.microsoftonline.com/${azTenantId?.value}`,
this.az_logout_uri = `https://login.microsoftonline.com/${azTenantId?.value}/oauth2/v2.0/logout`;
this.redis_password = redisPassword?.value || '';
} catch (err) {
console.error("🥞 app_config - server - updateValues - failed");
Expand All @@ -109,7 +115,9 @@ export default class AppConfig{
this.app_reg_client_id = process.env.APP_REG_CLIENT_ID;
this.app_client_secret = process.env.APP_CLIENT_SECRET;
this.api_scope = process.env.API_SCOPE;
this.api_scope_default = `api://${process.env.API_SCOPE}/.default`;
this.az_authority = `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
this.az_logout_uri = `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/logout`;
this.redis_password = process.env.REDIS_PASSWORD;
};
}
Expand Down
112 changes: 58 additions & 54 deletions frontend_svelte/src/lib/server/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
import AppConfig from './config';
import { ConfidentialClientApplication, type AuthenticationResult } from '@azure/msal-node';
import type { Session } from '$lib/types';
import { building } from '$app/environment';

const scopes = ["User.Read"];
const appConfig = await AppConfig.getInstance();
const scopes = [appConfig.api_scope_default]

let msalConfClient: ConfidentialClientApplication | null = null;

const createMsalConfClient = async () => {
if (!msalConfClient){
// const configuration = await app_config();
const appConfig = await AppConfig.getInstance();
console.log("👍 🔥oauth - Authentication - MsalConfClient - created!");
// const appConfig = await AppConfig.getInstance();
// console.log(appConfig.keyvault_health)

const msalConfig = {
Expand All @@ -33,11 +34,36 @@ const createMsalConfClient = async () => {
}

msalConfClient = new ConfidentialClientApplication(msalConfig);
console.log("👍 🔥oauth - Authentication - MsalConfClient - created!");
}
return msalConfClient;
}

export const signIn = async ( origin: string): Promise<string> => {
if (!building){
try{
await createMsalConfClient()
} catch (err) {
console.error("🔥 oauth - getTokens - msalConfClient could not created");
throw err;
}
}

const checkMsalConfClient = async () => {
if (!msalConfClient){
try{
await createMsalConfClient()
} catch (err) {
console.error("🔥 oauth - getTokens - msalConfClient could not created");
throw err;
}
}
if (!msalConfClient){
throw new Error("🔥 oauth - getTokens failed - msalConfClient not initialized");
}
return msalConfClient
}

export const signIn = async ( origin: string, scopes: [string] = ["User.Read"] ): Promise<string> => {
// Check if msalClient exists on very first login, if not create it.
// const appConfig = AppConfig.getInstance();
// console.log("oauth - Authentication - signIn - appConfig: ");
Expand All @@ -49,17 +75,7 @@ export const signIn = async ( origin: string): Promise<string> => {
};
let authCodeUrl: string
try {
if (!msalConfClient){
try{
await createMsalConfClient()
} catch (err) {
console.error("🔥 oauth - signIn - msalConfClient could not created");
throw err;
}
}
if (!msalConfClient){
throw new Error("🔥 oauth - signIn failed - msalConfClient not initialized");
}
const msalConfClient = await checkMsalConfClient()
authCodeUrl = await msalConfClient.getAuthCodeUrl(authCodeUrlParameters);
} catch (err) {
console.error("🔥 oauth - Authentication - signIn failed");
Expand All @@ -69,22 +85,12 @@ export const signIn = async ( origin: string): Promise<string> => {
return authCodeUrl;
}

export const getTokens = async(code: string | null, origin: string): Promise<AuthenticationResult> => {
export const authenticateWithCode = async(code: string | null, origin: string, scopes: [string] = ["User.Read"]): Promise<AuthenticationResult> => {
if (!code) {
throw new Error("🔥 oauth - GetAccessToken failed - no code");
}
try {
if (!msalConfClient){
try{
await createMsalConfClient()
} catch (err) {
console.error("🔥 oauth - getTokens - msalConfClient could not created");
throw err;
}
}
if (!msalConfClient){
throw new Error("🔥 oauth - getTokens failed - msalConfClient not initialized");
}
const msalConfClient = await checkMsalConfClient()
const response = await msalConfClient.acquireTokenByCode({
code: code,
scopes: scopes,
Expand All @@ -100,40 +106,38 @@ export const getTokens = async(code: string | null, origin: string): Promise<Aut
}
}

export const getAccessToken = async ( sessionData: Session ): Promise<string> => {
if (!msalConfClient){
try{
await createMsalConfClient()
} catch (err) {
console.error("🔥 oauth - getAccessToken - msalConfClient could not created");
throw err;
}
}
if (!msalConfClient){
throw new Error("🔥 oauth - getAccessToken failed - msalConfClient not initialized");
}
export const getAccessToken = async ( sessionData: Session, scopes: [string] = [appConfig.api_scope_default] ): Promise<string> => {
const msalConfClient = await checkMsalConfClient()
const account = sessionData.account;
const response = await msalConfClient.acquireTokenSilent({
try {
const response = await msalConfClient.acquireTokenSilent({
scopes: scopes,
account: account,
});
const accessToken = response.accessToken;
return accessToken
}

export const signOut = async ( ): Promise<void> => {
// TBD: implement logout
try {
if (!msalConfClient){
throw new Error("🔥 oauth - signIn failed - msalConfClient not initialized");
}
// implement logout
});
const accessToken = response.accessToken;
return accessToken
} catch (err) {
console.error("🔥 oauth - Logout failed: ", err);
console.error("🔥 oauth - GetAccessToken failed");
console.error(err);
throw err
}
}
}

export const getAccessTokenMsGraph = async ( sessionData: Session ): Promise<string> => {
const accessToken = await getAccessToken(sessionData, ["User.Read"])
return accessToken
}

// export const signOut = async ( ): Promise<void> => {
// // TBD: implement logout
// // try {
// // const msalConfClient = await checkMsalConfClient()
// // // implement logout
// // } catch (err) {
// // console.error("🔥 oauth - Logout failed: ", err);
// // throw err
// // }
// }


console.log("👍 🔥 lib - server - oauth.ts - end");
Loading

0 comments on commit fe0feea

Please sign in to comment.