Skip to content

fix(auth): Suunto - verify cryptographic signatures#540

Open
HuziHoic wants to merge 3 commits intothe-momentum:mainfrom
HuziHoic:suunto-oauth
Open

fix(auth): Suunto - verify cryptographic signatures#540
HuziHoic wants to merge 3 commits intothe-momentum:mainfrom
HuziHoic:suunto-oauth

Conversation

@HuziHoic
Copy link

@HuziHoic HuziHoic commented Feb 28, 2026

Closes #511

Description

Fixes a security vulnerability in the Suunto OAuth integration where JWT tokens were being accepted without signature verification.

Checklist

General

  • My code follows the project's code style
  • I have performed a self-review of my code
  • I have added tests that prove my fix/feature works (if applicable)
  • New and existing tests pass locally

Backend Changes

You have to be in backend directory to make it work:

  • uv run pre-commit run --all-files passes

Frontend Changes

  • pnpm run lint passes
  • pnpm run format:check passes
  • pnpm run build succeeds

Testing Instructions

Steps to test:

  1. Set up the platform with Suunto integration enabled
  2. Attempt to use a forged JWT token with the OAuth callback endpoint
  3. Observe that forged tokens are now rejected (previously they were accepted)

Expected behavior:

  • Forged/unsigned tokens are rejected with appropriate error handling

Screenshots

Additional Notes

Summary by CodeRabbit

  • Bug Fixes
    • Improved OAuth user info handling: added cryptographic verification of provider tokens (including key lookup and signature checks), fall-back decoding when needed, and more robust error handling to safely reject invalid or tampered tokens while preserving the public method behavior.

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Reworks Suunto OAuth user-info extraction to verify JWTs: decodes unverified header to get kid, fetches provider JWKS, builds a public key from the matching JWK, and verifies the token with RS256 and audience validation; errors return None for user_id/username.

Changes

Cohort / File(s) Summary
Suunto OAuth JWT Verification
backend/app/services/providers/suunto/oauth.py
Replaces unverified JWT extraction with RS256 verification: reads unverified header (kid), fetches JWKS via requests (optional subscription header), selects matching key, constructs public key using jose.jwk, verifies token with audience check via jose and handles errors (returns None on failure). Adds requests, jose.jwk, jose.exceptions imports and handle_exceptions decorator on _get_provider_user_info. Public method signatures unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SuuntoService
    participant JWKS as "Suunto JWKS Endpoint"
    participant JWKLib as "jose jwk / crypto"

    Client->>SuuntoService: provide access token
    SuuntoService->>SuuntoService: decode unverified header -> extract kid
    SuuntoService->>JWKS: GET /.well-known/jwks.json (optional subscription header)
    JWKS-->>SuuntoService: return JWKS (keys)
    SuuntoService->>JWKLib: select key by kid, build public key
    SuuntoService->>JWKLib: verify JWT (RS256) with audience
    JWKLib-->>SuuntoService: claims or verification error
    SuuntoService-->>Client: return user info or null on failure
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
I nibbled headers, found the key,
Pulled JWKS down from up the tree,
RS256 now seals the deal,
I twitch my whiskers — cryptographic zeal,
Secure and snug — a rabbit's plea!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding JWT signature verification to the Suunto OAuth implementation, which is the core security fix addressed in this PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/providers/suunto/oauth.py (1)

33-45: ⚠️ Potential issue | 🟠 Major

Avoid silent catch-all handling in this auth-verification path.

Line 44 catches all exceptions and returns null identifiers, which hides JWKS/config failures and removes actionable observability. Please move to the service exception-handling pattern (@handle_exceptions) or at least narrow and log failures.

As per coding guidelines, "backend/app/services/**/*.py: Services layer must contain business logic and never perform database operations directly; use @handle_exceptions decorator for error handling".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 33 - 45, The
current try/except around PyJWKClient.get_signing_key_from_jwt and jwt.decode
swallows all errors; remove the bare "except Exception" and adopt the service
error pattern by adding the `@handle_exceptions` decorator to the function that
performs this verification (the function in oauth.py that reads
token_response.access_token and returns {"user_id", "username"}), or if you must
handle locally, catch specific exceptions (e.g., PyJWKClient/get_signing_key
errors, jwt.InvalidTokenError, requests exceptions) and log them with the
service logger before returning the fallback; ensure you reference
PyJWKClient.get_signing_key_from_jwt, jwt.decode, token_response.access_token
and use `@handle_exceptions` on the enclosing function to follow the service layer
pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/services/providers/suunto/oauth.py`:
- Line 2: The import of PyJWKClient from jose.jwk is invalid for this project
(we use python-jose), so either switch to PyJWT end-to-end (replace the import
with jwt.PyJWKClient and use PyJWT APIs throughout) or adapt to python-jose by
removing PyJWKClient usage and implement JWKS handling yourself: fetch the JWKS
JSON, locate the key by kid, call jose.jwk.construct(...) to build the key, and
pass that to jose.jwt.decode(...). Also remove the bare `except Exception:`
currently around the JWKS retrieval/verification and instead annotate the
function that does JWKS fetching/validation with the `@handle_exceptions`
decorator (replace the try/except block inside the JWKS-related function with
the decorator) so errors are handled per service guidelines.

---

Outside diff comments:
In `@backend/app/services/providers/suunto/oauth.py`:
- Around line 33-45: The current try/except around
PyJWKClient.get_signing_key_from_jwt and jwt.decode swallows all errors; remove
the bare "except Exception" and adopt the service error pattern by adding the
`@handle_exceptions` decorator to the function that performs this verification
(the function in oauth.py that reads token_response.access_token and returns
{"user_id", "username"}), or if you must handle locally, catch specific
exceptions (e.g., PyJWKClient/get_signing_key errors, jwt.InvalidTokenError,
requests exceptions) and log them with the service logger before returning the
fallback; ensure you reference PyJWKClient.get_signing_key_from_jwt, jwt.decode,
token_response.access_token and use `@handle_exceptions` on the enclosing function
to follow the service layer pattern.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0872b7 and 2ace57a.

📒 Files selected for processing (1)
  • backend/app/services/providers/suunto/oauth.py

@@ -1,4 +1,5 @@
from jose import jwt
from jose.jwk import PyJWKClient
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n backend/app/services/providers/suunto/oauth.py

Repository: the-momentum/open-wearables

Length of output: 2415


🌐 Web query:

Does python-jose expose PyJWKClient or is it a PyJWT API?

💡 Result:

PyJWKClient is a PyJWT API (it’s documented as jwt.PyJWKClient in PyJWT’s API reference). (pyjwt.readthedocs.io)

python-jose does not expose PyJWKClient; instead it has its own jose.jwt / jose.jwk APIs (e.g., jose.jwk.construct(...) for working with JWK material), and if you need JWKS-from-URL behavior you typically fetch the JWKS yourself and then construct/select the key. (python-jose.readthedocs.io)

Citations:


🏁 Script executed:

# Check declared dependencies
find . -type f \( -name "pyproject.toml" -o -name "poetry.lock" -o -name "requirements*.txt" -o -name "Pipfile*" \) \
  | head -5 \
  | xargs -r ls -lh

echo "=== Checking for JWT library declarations ==="
rg -i 'python-jose|pyjwt' -t toml -t lock -t txt 2>/dev/null | head -20 || true

Repository: the-momentum/open-wearables

Length of output: 1289


🏁 Script executed:

# Check the base class and error handling pattern used in services
cat -n backend/app/services/providers/templates/base_oauth.py | head -50

Repository: the-momentum/open-wearables

Length of output: 1867


Resolve the JWT library mismatch—this import will fail at runtime.

Line 2 imports PyJWKClient from jose.jwk, but the project declares python-jose (not PyJWT) in backend/pyproject.toml. PyJWKClient is a PyJWT API that doesn't exist in python-jose, so this line will raise an ImportError or AttributeError at import time, preventing the entire module from loading.

Switch to one consistent JWT library: either use PyJWT end-to-end with jwt.PyJWKClient, or use python-jose with its own JWKS key handling approach (e.g., fetch JWKS manually and use jose.jwk.construct(...)).

Additionally, replace the bare except Exception: at line 44–45 with the @handle_exceptions decorator per service layer guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` at line 2, The import of
PyJWKClient from jose.jwk is invalid for this project (we use python-jose), so
either switch to PyJWT end-to-end (replace the import with jwt.PyJWKClient and
use PyJWT APIs throughout) or adapt to python-jose by removing PyJWKClient usage
and implement JWKS handling yourself: fetch the JWKS JSON, locate the key by
kid, call jose.jwk.construct(...) to build the key, and pass that to
jose.jwt.decode(...). Also remove the bare `except Exception:` currently around
the JWKS retrieval/verification and instead annotate the function that does JWKS
fetching/validation with the `@handle_exceptions` decorator (replace the
try/except block inside the JWKS-related function with the decorator) so errors
are handled per service guidelines.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
backend/app/services/providers/suunto/oauth.py (3)

56-61: Consider adding issuer validation for defense in depth.

The JWT is correctly verified with RS256 signature and audience validation. For additional security hardening, consider also validating the iss (issuer) claim to ensure the token originates from the expected Suunto authorization server.

🛡️ Optional issuer validation
         decoded = jwt.decode(
             token_response.access_token,
-            public_pem,
+            matching,
             algorithms=["RS256"],
             audience=settings.suunto_client_id,
+            issuer="https://cloudapi-oauth.suunto.com",
         )

Note: Verify the exact issuer value from Suunto's documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 56 - 61, The JWT
decode call currently validates signature and audience but lacks issuer
checking; update the jwt.decode invocation that processes
token_response.access_token (using public_pem and settings.suunto_client_id) to
also validate the iss claim by passing the expected issuer (e.g.
settings.suunto_issuer) to jwt.decode (or via options/issuer param), and ensure
settings.suunto_issuer matches Suunto's documented issuer value.

39-46: Consider caching JWKS to improve performance and reliability.

The JWKS is fetched from the remote endpoint on every token verification request. This adds latency and creates a dependency on the external endpoint's availability for each OAuth callback.

Consider caching the JWKS with a reasonable TTL (e.g., 1 hour) to reduce network calls and improve resilience. Libraries like cachetools or a simple module-level cache with expiration could work here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 39 - 46, The
JWKS is being fetched on every verification (jwks_url via requests.get /
resp.json), causing latency and fragility; implement a module-level cache (e.g.,
JWKS_CACHE or similar) that stores the JWKS payload and a fetched timestamp and
check TTL (e.g., 3600s) before calling requests.get again, returning cached
jwks.get("keys", []) when fresh; on fetch failure, fall back to the cached value
if present and not expired or raise the original error only if no valid cache
exists; update the code paths that use resp, jwks, keys, and matching to read
from the cache abstraction instead of always calling requests.get.

48-54: Narrow the exception handling for PEM conversion.

The bare except Exception: at line 52 is overly broad and masks the actual error if key construction fails. Since python-jose's jwt.decode() accepts JWK dicts directly (a supported pattern), the fallback to matching will work. However, catching all exceptions obscures whether the failure is due to an unexpected key type, malformed key, or a legitimate error.

Either catch the specific exception (e.g., AttributeError if to_pem() is unavailable) or simplify by passing the JWK dict directly:

♻️ Proposed simplification
-        # Construct a key object and obtain a PEM representation for decoding
-        public_key = jwk.construct(matching)
-        try:
-            public_pem = public_key.to_pem()
-        except Exception:
-            # Fallback: if to_pem is not available, pass the jwk dict directly
-            public_pem = matching
-
         decoded = jwt.decode(
             token_response.access_token,
-            public_pem,
+            matching,
             algorithms=["RS256"],
             audience=settings.suunto_client_id,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 48 - 54, The
except block that currently catches all exceptions when calling
public_key.to_pem() is too broad; instead either catch the specific exception
(e.g., AttributeError) thrown when to_pem() is not present, or remove the
try/except and always pass the JWK dict `matching` to downstream decode logic
(since python-jose accepts JWK dicts). Update the code around
jwk.construct(matching) and the public_key.to_pem() call to only handle
AttributeError (or remove the fallback entirely) and ensure `public_pem` is
either the PEM bytes when to_pem() exists or the original `matching` dict
otherwise, so jwt.decode() receives a supported key representation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/app/services/providers/suunto/oauth.py`:
- Around line 56-61: The JWT decode call currently validates signature and
audience but lacks issuer checking; update the jwt.decode invocation that
processes token_response.access_token (using public_pem and
settings.suunto_client_id) to also validate the iss claim by passing the
expected issuer (e.g. settings.suunto_issuer) to jwt.decode (or via
options/issuer param), and ensure settings.suunto_issuer matches Suunto's
documented issuer value.
- Around line 39-46: The JWKS is being fetched on every verification (jwks_url
via requests.get / resp.json), causing latency and fragility; implement a
module-level cache (e.g., JWKS_CACHE or similar) that stores the JWKS payload
and a fetched timestamp and check TTL (e.g., 3600s) before calling requests.get
again, returning cached jwks.get("keys", []) when fresh; on fetch failure, fall
back to the cached value if present and not expired or raise the original error
only if no valid cache exists; update the code paths that use resp, jwks, keys,
and matching to read from the cache abstraction instead of always calling
requests.get.
- Around line 48-54: The except block that currently catches all exceptions when
calling public_key.to_pem() is too broad; instead either catch the specific
exception (e.g., AttributeError) thrown when to_pem() is not present, or remove
the try/except and always pass the JWK dict `matching` to downstream decode
logic (since python-jose accepts JWK dicts). Update the code around
jwk.construct(matching) and the public_key.to_pem() call to only handle
AttributeError (or remove the fallback entirely) and ensure `public_pem` is
either the PEM bytes when to_pem() exists or the original `matching` dict
otherwise, so jwt.decode() receives a supported key representation.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ace57a and dcc52e2.

📒 Files selected for processing (1)
  • backend/app/services/providers/suunto/oauth.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/app/services/providers/suunto/oauth.py (1)

90-91: Redundant exception types and overly broad catch.

The exception tuple (JWTError, JWSError, requests.RequestException, Exception) is redundant since Exception catches everything, making the specific types pointless. This also masks unexpected errors. Consider catching only the specific expected exceptions.

♻️ Proposed fix
-        except (JWTError, JWSError, requests.RequestException, Exception):
+        except (JWTError, JWSError, requests.RequestException, KeyError, ValueError):
             return {"user_id": None, "username": None}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 90 - 91, The
current except clause in backend/app/services/providers/suunto/oauth.py is too
broad (it catches Exception), so change it to catch only the expected
exceptions: JWTError, JWSError, and requests.RequestException (e.g. "except
(JWTError, JWSError, requests.RequestException) as e:"), remove the generic
Exception from the tuple, and handle the error by logging the exception (using
the existing logger or logger.exception) before returning {"user_id": None,
"username": None} to avoid swallowing unexpected errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/services/providers/suunto/oauth.py`:
- Around line 40-51: The HS256 branch in oauth.py currently decodes
token_response.access_token with key=None and options={"verify_signature":
False} (the jwt.decode call) which accepts forged tokens; remove that unverified
HS256 path or change it to verify using the shared secret (use the configured
client_secret when algorithm == "HS256") by passing the secret as the key and
not disabling signature verification, and validate claims as you do for RS256;
also replace the bare "except Exception: pass" with explicit error handling (log
the exception and return None or re-raise) so failures in the jwt.decode/token
parsing are not silently swallowed.

---

Nitpick comments:
In `@backend/app/services/providers/suunto/oauth.py`:
- Around line 90-91: The current except clause in
backend/app/services/providers/suunto/oauth.py is too broad (it catches
Exception), so change it to catch only the expected exceptions: JWTError,
JWSError, and requests.RequestException (e.g. "except (JWTError, JWSError,
requests.RequestException) as e:"), remove the generic Exception from the tuple,
and handle the error by logging the exception (using the existing logger or
logger.exception) before returning {"user_id": None, "username": None} to avoid
swallowing unexpected errors.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcc52e2 and 18b21bc.

📒 Files selected for processing (1)
  • backend/app/services/providers/suunto/oauth.py

Comment on lines +40 to +51
if algorithm == "HS256":
decoded = jwt.decode(
token_response.access_token,
key=None,
options={"verify_signature": False},
)
provider_username = decoded.get("user")
provider_user_id = decoded.get("sub")
if provider_user_id is not None:
return {"user_id": provider_user_id, "username": provider_username}
except Exception:
pass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: HS256 branch bypasses signature verification entirely.

This code path decodes HS256 tokens without any signature verification (key=None, verify_signature: False). An attacker can craft a malicious JWT with alg: HS256 and arbitrary claims—the code will accept it. This completely defeats the security fix this PR intends to provide.

Either:

  1. Remove this unverified HS256 path entirely, or
  2. Verify HS256 tokens using the appropriate shared secret (client_secret)

Additionally, the bare except Exception: pass at lines 50-51 silently swallows all errors, masking potential issues.

🔒 Option 1: Remove the insecure HS256 path (recommended if Suunto only issues RS256 tokens)
     `@handle_exceptions`
     def _get_provider_user_info(self, token_response: OAuthTokenResponse, user_id: str) -> dict[str, str | None]:
         """Extracts and verifies Suunto user info from JWT access token."""
-        try:
-            unverified_header = jwt.get_unverified_header(token_response.access_token)
-            algorithm = unverified_header.get("alg")
-            
-            if algorithm == "HS256":
-                decoded = jwt.decode(
-                    token_response.access_token,
-                    key=None,
-                    options={"verify_signature": False},
-                )
-                provider_username = decoded.get("user")
-                provider_user_id = decoded.get("sub")
-                if provider_user_id is not None:
-                    return {"user_id": provider_user_id, "username": provider_username}
-        except Exception:
-            pass
-
         try:
             # Get the key ID from token header
🔒 Option 2: Properly verify HS256 with the client secret
         try:
             unverified_header = jwt.get_unverified_header(token_response.access_token)
             algorithm = unverified_header.get("alg")
             
             if algorithm == "HS256":
                 decoded = jwt.decode(
                     token_response.access_token,
-                    key=None,
-                    options={"verify_signature": False},
+                    key=self.credentials.client_secret,
+                    algorithms=["HS256"],
+                    audience=settings.suunto_client_id,
                 )
                 provider_username = decoded.get("user")
                 provider_user_id = decoded.get("sub")
                 if provider_user_id is not None:
                     return {"user_id": provider_user_id, "username": provider_username}
-        except Exception:
-            pass
+        except (JWTError, JWSError):
+            pass  # Fall through to RS256 verification
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/providers/suunto/oauth.py` around lines 40 - 51, The
HS256 branch in oauth.py currently decodes token_response.access_token with
key=None and options={"verify_signature": False} (the jwt.decode call) which
accepts forged tokens; remove that unverified HS256 path or change it to verify
using the shared secret (use the configured client_secret when algorithm ==
"HS256") by passing the secret as the key and not disabling signature
verification, and validate claims as you do for RS256; also replace the bare
"except Exception: pass" with explicit error handling (log the exception and
return None or re-raise) so failures in the jwt.decode/token parsing are not
silently swallowed.

Copy link
Collaborator

@KaliszS KaliszS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to fix code quality issues. You can check them locally by running uv run pre-commit run --all-files from backend directory.

Also you have asked for algorithm used in jwkts by Suunto. They didn't provide this information in docs, but we can always decode JWT access token and thats HS256

Image

@HuziHoic
Copy link
Author

HuziHoic commented Mar 5, 2026

We need to fix code quality issues. You can check them locally by running uv run pre-commit run --all-files from backend directory.

Also you have asked for algorithm used in jwkts by Suunto. They didn't provide this information in docs, but we can always decode JWT access token and thats HS256

Image

Thank you, I will fix the code quality issue and decode using HS256. Additionally, the test test_extract_user_info_from_jwt_success currently encodes the JWT with "secret" but the updated implementation now verifies the signature using self.credentials.client_secret. Should we update the test to encode with settings.suunto_client_secret to match the real flow, and do the same for test_extract_user_info_from_jwt_missing_fields for consistency?

Copy link
Collaborator

@KaliszS KaliszS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great for tests to match real flow, but we don't want to use real secrets from .env within tests in my opinion.

There are also a lot of errors in code-quality Ci step. They need to be fixed after the review changes will be implemented.

unverified_header = jwt.get_unverified_header(token_response.access_token)
algorithm = unverified_header.get("alg")

if algorithm == "HS256":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As code rabbit say - HS256 is the only option. So there should be just one path to solve the issue.

Copy link
Collaborator

@KaliszS KaliszS Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can just add something like if algorithm =! "HS256" and then raise exception "Not implemented" so we will know whether Suunto will change their algorithm.

)
provider_username = decoded.get("user")
provider_user_id = decoded.get("sub")
if provider_user_id is not None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess just if provider_user_id will be enough. If that would be empty string, 0 or [], that also should raise exception. So it's safe to do that this way.

key="", # Empty key since we're not verifying
options={"verify_signature": False},
matching_key,
algorithms=["RS256"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That RS256 usage is redundant, please simplify it to one simple workflow.

if self.credentials.subscription_key:
headers["Ocp-Apim-Subscription-Key"] = self.credentials.subscription_key

resp = requests.get(jwks_url, headers=headers, timeout=5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should implement some caching strategy to avoid making this request every time user is logging? lru_cache should work fine.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, we should probably drop that support anyway. HS256 is used by Suunto.

if algorithm == "HS256":
decoded = jwt.decode(
token_response.access_token,
key=None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but HS256 is symmtric algorithm. So we can just probaly use here client_secret? Or subscription key, that should be tested manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security concern in Suunto OAuth integration

2 participants