Skip to content

Commit 8faf250

Browse files
committed
cache changes
1 parent 282fac5 commit 8faf250

4 files changed

Lines changed: 69 additions & 43 deletions

File tree

lib/console/ai/tools/workbench/integration/teams/reply.ex

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,12 @@ defmodule Console.AI.Tools.Workbench.Integration.Teams.Reply do
3333
end
3434

3535
def implement(%__MODULE__{tool: %WorkbenchTool{configuration: %Configuration{teams: %TeamsConnection{} = conn}}, text: text}) do
36-
case reply_context() do
37-
%ChatbotMessage{service_url: url, conversation_id: cid} when is_binary(url) and is_binary(cid) ->
38-
case Connector.reply(conn, url, cid, text) do
39-
{:ok, resp} -> Jason.encode(resp)
40-
{:error, reason} -> {:error, reason}
41-
end
42-
_ ->
43-
{:error, "no teams chat context is available for this job; use teams_post_channel_message with explicit ids instead"}
36+
with %ChatbotMessage{service_url: url, conversation_id: cid} when is_binary(url) and is_binary(cid) <- reply_context(),
37+
{:ok, resp} <- Connector.reply(conn, url, cid, text) do
38+
Jason.encode(resp)
39+
else
40+
{:error, _} = err -> err
41+
_ -> {:error, "no teams chat context is available for this job; use teams_post_channel_message with explicit ids instead"}
4442
end
4543
end
4644
def implement(%__MODULE__{}), do: {:error, "Microsoft Teams app registration is not configured for this workbench tool."}

lib/console/chat/teams/auth.ex

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ defmodule Console.Chat.Teams.Auth do
1111
to discover the metadata, load + cache the JWKS, and validate the signature/issuer/audience/lifetime. The
1212
Teams-specific `serviceurl` claim is checked separately.
1313
"""
14-
use Nebulex.Caching
15-
require Logger
14+
alias Console.OIDC.ProviderConfiguration
1615

1716
# The connector publishes its metadata here. Two quirks are needed to consume it via oidcc:
1817
# * the document's declared issuer is `https://api.botframework.com` (not this url) -> allow_issuer_mismatch
1918
# * the document omits several oidc-required fields (scopes/response_types/subject_types), so we backfill
2019
# them via document_overrides purely to satisfy the parser - they do not affect token validation.
2120
@config_issuer "https://login.botframework.com/v1"
22-
@cache_adapter Console.conf(:cache_adapter)
23-
@ttl :timer.hours(1)
2421

2522
@quirks %{
2623
quirks: %{
@@ -43,7 +40,7 @@ defmodule Console.Chat.Teams.Auth do
4340
def verify(token, audience, opts \\ [])
4441
def verify(token, audience, opts) when is_binary(token) and is_binary(audience) do
4542
with {:ok, _} <- peek(token),
46-
{:ok, {conf, jwks}} <- provider_configuration(),
43+
{:ok, {conf, jwks}} <- ProviderConfiguration.fetch(@config_issuer, @quirks),
4744
ctx = Oidcc.ClientContext.from_manual(conf, jwks, audience, "dummy_secret", %{client_jwks: JOSE.JWK.generate_key(16)}),
4845
validate_opts = %{signing_algs: ctx.provider_configuration.id_token_signing_alg_values_supported},
4946
{:ok, claims} <- validate_jwt(token, ctx, validate_opts),
@@ -53,22 +50,6 @@ defmodule Console.Chat.Teams.Auth do
5350
end
5451
def verify(_, _, _), do: {:error, "missing teams bot token or audience"}
5552

56-
@doc """
57-
Loads and caches the Bot Framework provider configuration + JWKS. Cached so we don't round-trip Microsoft on
58-
every webhook; the ttl also bounds how stale the signing keys can get across a key rotation.
59-
"""
60-
@decorate cacheable(cache: @cache_adapter, key: :teams_bf_oidc_config, opts: [ttl: @ttl], match: &ok?/1)
61-
def provider_configuration() do
62-
with {:ok, {conf, _}} <- Oidcc.ProviderConfiguration.load_configuration(@config_issuer, @quirks),
63-
{:ok, {jwks, _}} <- Oidcc.ProviderConfiguration.load_jwks(conf.jwks_uri) do
64-
{:ok, {conf, jwks}}
65-
else
66-
err ->
67-
Logger.warning("failed to load teams bot framework oidc configuration: #{inspect(err)}")
68-
{:error, "could not load teams bot framework configuration"}
69-
end
70-
end
71-
7253
# cheap, network-free rejection of obviously malformed tokens before we touch the provider config
7354
defp peek(token) do
7455
case Joken.peek_header(token) do
@@ -90,9 +71,6 @@ defmodule Console.Chat.Teams.Auth do
9071
do: check(String.trim_trailing(claim, "/") == String.trim_trailing(url, "/"), "teams jwt serviceUrl mismatch")
9172
defp validate_service_url(_claims, _url), do: {:error, "teams jwt is missing the serviceUrl claim"}
9273

93-
defp ok?({:ok, _}), do: true
94-
defp ok?(_), do: false
95-
9674
defp check(true, _), do: :ok
9775
defp check(_, msg), do: {:error, msg}
9876
end

lib/console/deployments/settings.ex

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -419,18 +419,10 @@ defmodule Console.Deployments.Settings do
419419
}
420420

421421
@doc """
422-
Fetches the issuer configuration from the issuer url
422+
Fetches (and caches) the issuer configuration + jwks from the issuer url
423423
"""
424-
@decorate cacheable(
425-
cache: @cache_adapter,
426-
key: {:issuer_configuration, issuer},
427-
opts: [ttl: :timer.minutes(60)]
428-
)
429-
def issuer_configuration(issuer) do
430-
with {:ok, {conf, _}} <- Oidcc.ProviderConfiguration.load_configuration(issuer, @quirks),
431-
{:ok, {jwks, _}} <- Oidcc.ProviderConfiguration.load_jwks(conf.jwks_uri),
432-
do: {:ok, {conf, jwks}}
433-
end
424+
def issuer_configuration(issuer),
425+
do: Console.OIDC.ProviderConfiguration.fetch(issuer, @quirks)
434426

435427
@decorate cache_evict(cache: @cache_adapter, key: :deployment_settings)
436428
def update(attrs) do
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
defmodule Console.OIDC.ProviderConfiguration do
2+
@moduledoc """
3+
Fetches and caches an OpenID Connect provider's configuration + JWKS for an issuer.
4+
5+
`oidcc` normally recommends running a supervised `Oidcc.ProviderConfiguration.Worker` per provider (an in-memory
6+
GenServer that background-refreshes). We instead cache through Nebulex so the result is shared across the
7+
cluster and we don't need a worker per issuer in the supervision tree. The cache ttl is derived from the
8+
`Cache-Control` max-age the provider advertises on its metadata/JWKS responses (`oidcc` returns this as a
9+
relative millisecond expiry, falling back to its own 15m default), so entries refresh about when the signing
10+
keys would otherwise go stale. We take the sooner of the config/jwks expiries (jwks rotation is what matters
11+
for signature validation) and clamp it to a sane window.
12+
13+
Callers pass their own `quirks` opts map (`%{quirks: %{...}}`) for atypical providers - e.g. document overrides
14+
or `allow_issuer_mismatch` - since those are provider-specific.
15+
"""
16+
require Logger
17+
18+
@cache_adapter Console.conf(:cache_adapter)
19+
@min_ttl :timer.minutes(5)
20+
@max_ttl :timer.hours(24)
21+
22+
@type provider :: {Oidcc.ProviderConfiguration.t(), :jose_jwk.key()}
23+
24+
@doc """
25+
Loads (and caches) the provider configuration + jwks for `issuer`. `quirks` is forwarded to
26+
`Oidcc.ProviderConfiguration.load_configuration/2`.
27+
"""
28+
@spec fetch(binary, map) :: {:ok, provider} | {:error, term}
29+
def fetch(issuer, quirks \\ %{}) when is_binary(issuer) do
30+
case @cache_adapter.get(key(issuer)) do
31+
{_conf, _jwks} = hit -> {:ok, hit}
32+
_ -> refresh(issuer, quirks)
33+
end
34+
end
35+
36+
defp refresh(issuer, quirks) do
37+
with {:ok, {conf, conf_exp}} <- Oidcc.ProviderConfiguration.load_configuration(issuer, quirks),
38+
{:ok, {jwks, jwks_exp}} <- Oidcc.ProviderConfiguration.load_jwks(conf.jwks_uri) do
39+
@cache_adapter.put(key(issuer), {conf, jwks}, ttl: ttl(conf_exp, jwks_exp))
40+
{:ok, {conf, jwks}}
41+
else
42+
err ->
43+
Logger.warning("failed to load oidc provider configuration for #{issuer}: #{inspect(err)}")
44+
normalize(err)
45+
end
46+
end
47+
48+
defp normalize({:error, _} = err), do: err
49+
defp normalize(err), do: {:error, err}
50+
51+
defp key(issuer), do: {:oidc_provider_config, issuer}
52+
53+
defp ttl(conf_exp, jwks_exp) do
54+
min(conf_exp, jwks_exp)
55+
|> max(@min_ttl)
56+
|> min(@max_ttl)
57+
end
58+
end

0 commit comments

Comments
 (0)