11# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
22"""OAuth 2.0 endpoints for API V2"""
33
4+ import base64
45import logging
56import os
67from datetime import datetime , timedelta
@@ -53,14 +54,12 @@ async def _parse_token_request(http_request: Request) -> TokenRequest:
5354 basic_client_secret = ""
5455 auth_header = http_request .headers .get ("authorization" , "" )
5556 if auth_header .startswith ("Basic " ):
56- import base64
57-
5857 try :
5958 decoded = base64 .b64decode (auth_header [6 :]).decode ("utf-8" )
6059 if ":" in decoded :
6160 basic_client_id , basic_client_secret = decoded .split (":" , 1 )
62- except (ValueError , UnicodeDecodeError ):
63- pass
61+ except (ValueError , UnicodeDecodeError ) as e :
62+ _logger . debug ( "Failed to decode Basic Auth header: %s" , e )
6463
6564 content_type = http_request .headers .get ("content-type" , "" )
6665 if "application/x-www-form-urlencoded" in content_type :
@@ -75,8 +74,8 @@ async def _parse_token_request(http_request: Request) -> TokenRequest:
7574 try :
7675 body = await http_request .json ()
7776 return TokenRequest (** body )
78- except Exception :
79- pass
77+ except Exception as e :
78+ _logger . debug ( "Could not parse token request from JSON body, falling back: %s" , e )
8079
8180 # Fall back to Basic Auth only (grant_type defaults to client_credentials)
8281 if basic_client_id :
@@ -126,9 +125,14 @@ async def get_token(
126125 detail = "Invalid client credentials" ,
127126 )
128127
128+ # Read configurable token lifetime (default: 24 hours for long-lived sessions)
129+ # nosemgrep: odoo-sudo-without-context
130+ token_lifetime_hours = int (env ["ir.config_parameter" ].sudo ().get_param ("spp_api_v2.token_lifetime_hours" , "24" ))
131+ expires_in = token_lifetime_hours * 3600
132+
129133 # Generate JWT token
130134 try :
131- token = _generate_jwt_token (env , api_client )
135+ token = _generate_jwt_token (env , api_client , token_lifetime_hours )
132136 except Exception as e :
133137 _logger .exception ("Error generating JWT token" )
134138 raise HTTPException (
@@ -139,11 +143,6 @@ async def get_token(
139143 # Build scope string from client scopes
140144 scope_str = " " .join (f"{ s .resource } :{ s .action } " for s in api_client .scope_ids )
141145
142- # Read configurable token lifetime (default: 24 hours for long-lived sessions)
143- # nosemgrep: odoo-sudo-without-context
144- token_lifetime_hours = int (env ["ir.config_parameter" ].sudo ().get_param ("spp_api_v2.token_lifetime_hours" , "24" ))
145- expires_in = token_lifetime_hours * 3600
146-
147146 return TokenResponse (
148147 access_token = token ,
149148 token_type = "Bearer" ,
@@ -189,14 +188,14 @@ def _get_jwt_secret(env: Environment) -> str:
189188 return secret
190189
191190
192- def _generate_jwt_token (env : Environment , api_client ) -> str :
191+ def _generate_jwt_token (env : Environment , api_client , token_lifetime_hours : int ) -> str :
193192 """
194193 Generate JWT access token for API client.
195194
196195 Token contains:
197196 - client_id (external identifier, NOT database ID)
198197 - scopes
199- - expiration (1 hour)
198+ - expiration time determined by token_lifetime_hours
200199
201200 SECURITY: Never include database IDs in JWT.
202201 The auth middleware loads the full api_client record from DB using client_id.
@@ -215,10 +214,6 @@ def _generate_jwt_token(env: Environment, api_client) -> str:
215214 # Build payload
216215 # SECURITY: Never include database IDs in JWT - use client_id only
217216 # The auth middleware looks up the full api_client record using client_id
218- # Read configurable token lifetime (default: 24 hours)
219- # nosemgrep: odoo-sudo-without-context
220- token_lifetime_hours = int (env ["ir.config_parameter" ].sudo ().get_param ("spp_api_v2.token_lifetime_hours" , "24" ))
221-
222217 now = datetime .utcnow ()
223218 payload = {
224219 "iss" : "openspp-api-v2" , # Issuer
0 commit comments