Skip to content

Commit

Permalink
Add support for interactive Entra ID authentication to chat_azure().
Browse files Browse the repository at this point in the history
This commit adds support for another major Azure authentication
approach: the OAuth authorization code flow, as used by the Azure CLI.

This is a good choice for authentiation during development on desktop,
and Microsoft recommends it for Azure OpenAI because it doesn't require
storing sensitive long-lived secrets like API keys.

All of this is pretty stock httr2 OAuth stuff, despite the fact that
Entra ID has its own... idiosyncrasies. I also went out of the way to
add a really specific error message for what I believe to be a common
source of problems: misconfiguration of Azure's RBAC. It looks as
follows:

    Error in `req_perform_connection()` at elmer/R/httr2.R:36:3:
    ! HTTP 401 Unauthorized.
    • PermissionDenied: Principal does not have access to API/Operation.
    ℹ Your user or service principal likely needs one of the following
      roles: Cognitive Services OpenAI User, Cognitive Services OpenAI
      Contributor, or Cognitive Services Contributor.

I haven't added any unit tests (I don't know how to do so for this kind
of interactive OAuth flow), but at least the help documentation has been
updated.

Signed-off-by: Aaron Jacobs <aaron.jacobs@posit.co>
  • Loading branch information
atheriel committed Jan 27, 2025
1 parent 68366da commit 63f259b
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 1 deletion.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
* The `token` argument to `chat_azure()` has been deprecated. Use ambient
credentials or the `credentials` argument instead (#257, @atheriel).

* `chat_azure()` attempts to use interactive Entra ID authentication if no other
credentials are available (#273, @atheriel).

# ellmer 0.1.0

* New `chat_vllm()` to chat with models served by vLLM (#140).
Expand Down
47 changes: 46 additions & 1 deletion R/provider-azure.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ NULL
#' `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` environment
#' variables are set.
#'
#' Finally, in interactive sessions it will also attempt to use Microsoft Entra
#' ID authentication -- much like the Azure CLI -- if no API key has been
#' provided.
#'
#' @param endpoint Azure OpenAI endpoint url with protocol and hostname, i.e.
#' `https://{your-resource-name}.openai.azure.com`. Defaults to using the
#' value of the `AZURE_OPENAI_ENDPOINT` envinronment variable.
Expand Down Expand Up @@ -137,7 +141,23 @@ method(chat_request, ProviderAzure) <- function(provider,
req <- req_retry(req, max_tries = 2)
req <- req_error(req, body = function(resp) {
error <- resp_body_json(resp)$error
paste0(error$code, ": ", error$message)
msg <- paste0(error$code, ": ", error$message)
# Try to be helpful in the (common) case that the user or service
# principal is missing the necessary role.
# See: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/role-based-access-control
if (error$message == "Principal does not have access to API/Operation.") {
msg <- c(
"*" = msg,
"i" = cli::format_inline(
"Your user or service principal likely needs one of the following
roles: {.emph Cognitive Services OpenAI User},
{.emph Cognitive Services OpenAI Contributor}, or
{.emph Cognitive Services Contributor}.",
keep_whitespace = FALSE
)
)
}
msg
})

messages <- compact(unlist(as_json(provider, turns), recursive = FALSE))
Expand Down Expand Up @@ -214,6 +234,31 @@ default_azure_credentials <- function(api_key = NULL, token = NULL) {
return(function() list())
}

# Masquerade as the Azure CLI.
client_id <- "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
if (is_interactive() && !is_hosted_session()) {
client <- oauth_client(
client_id,
token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
secret = "",
auth = "body",
name = paste0("ellmer-", client_id)
)
return(function() {
token <- oauth_token_cached(
client,
oauth_flow_auth_code,
flow_params = list(
auth_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
scope = "https://cognitiveservices.azure.com/.default offline_access",
redirect_uri = "http://localhost:8400",
auth_params = list(prompt = "select_account")
)
)
list(Authorization = paste("Bearer", token$access_token))
})
}

if (is_testing()) {
testthat::skip("no Azure credentials available")
}
Expand Down
4 changes: 4 additions & 0 deletions man/chat_azure.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 63f259b

Please sign in to comment.