API Authentication with Azure AD#
Now that we have Flask APIs running, let's add authentication using Azure AD.
Note
Azure Functions has built-in Identity feature, but for granular control over specific APIs and custom roles, we implement authentication in code.
OAuth2 Authentication Flow#
sequenceDiagram
participant Client
participant AzureAD
participant FunctionApp
Client->>AzureAD: Request Token (client_credentials)
AzureAD-->>Client: Access Token (JWT)
Client->>FunctionApp: API Request + Bearer Token
FunctionApp->>FunctionApp: Validate JWT
FunctionApp-->>Client: API Response
Create Azure AD App#
-
Navigate to Azure Portal → Azure Active Directory → App registrations → New Registration
-
Configure the app:
- Name:
functiondemo-ad-server-app - Supported Account Types: Single tenant
- Name:
-
Set Application ID URI:
- Copy
Application (client) ID - Go to Expose an API tab
- Set URI:
api://<<Application (client) ID>>
- Copy
-
Create Custom Role:
- Go to App Roles tab
- Create role:
GetSecret
Implementation#
Error Codes (src/configs/error_codes.py)#
from box import Box
def auth_error():
return {
"authorization_header_missing": {
"message": {"code": "AuthorizationHeaderMissing", "description": "Authorization header is missing"},
"status_code": 401
},
"token_expired": {
"message": {"code": "AuthorizationTokenExpired", "description": "Token is expired"},
"status_code": 401
},
"invalid_claims": {
"message": {"code": "InvalidAuthorizationClaims", "description": "Invalid claims"},
"status_code": 401
}
}
errors = Box(auth_error())
JWT Verification (src/authz/verify.py)#
from functools import wraps
import jwt
from flask import request
from jwt import PyJWKClient
from src.configs import adapp
def protected(f):
@wraps(f)
def decorated(*args, **kwargs):
token = get_token_auth_header()
jwk_client = PyJWKClient(adapp.JWT_URL)
signing_key = jwk_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=adapp.API_AUDIENCE,
issuer=adapp.ISSUER
)
if 'roles' not in payload:
raise AppError(errors.invalid_claims)
return f(*args, **kwargs)
return decorated
Protect Your Route#
from src.authz.verify import protected
@app.route("/vault", methods=['GET'])
@protected
def get_secret():
secret = request.args.get("secret")
return jsonify(message=f"Secret: {secret}"), 200
Test Authentication#
Without token:
{
"code": "AuthorizationHeaderMissing",
"description": "Authorization header is missing from the request"
}