From 6fc0b4bdaa045bd488ab2a6f73ad76e797f0db4a Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Wed, 22 Jan 2025 21:50:09 -0500 Subject: [PATCH] Add support for interactive Entra ID authentication to chat_azure(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- NEWS.md | 3 +++ R/provider-azure.R | 51 +++++++++++++++++++++++++++++++++++++++++++++- man/chat_azure.Rd | 4 ++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 4632264..2930ffc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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). diff --git a/R/provider-azure.R b/R/provider-azure.R index 7f3d669..4ac33df 100644 --- a/R/provider-azure.R +++ b/R/provider-azure.R @@ -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. @@ -137,7 +141,27 @@ 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 + bad_rbac <- identical( + error$message, + "Principal does not have access to API/Operation." + ) + if (bad_rbac) { + 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)) @@ -214,6 +238,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") } diff --git a/man/chat_azure.Rd b/man/chat_azure.Rd index be0032e..a97dc36 100644 --- a/man/chat_azure.Rd +++ b/man/chat_azure.Rd @@ -73,6 +73,10 @@ from OpenAI. picks up on Azure service principals automatically when the \code{AZURE_TENANT_ID}, \code{AZURE_CLIENT_ID}, and \code{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. } } \examples{