Run the OIDC Authorization Code + PKCE browser flow. Opens the user's browser to the Keycloak login page and waits for the authorization code redirect on a localhost callback server.
(
issuer: &str,
client_id: &str,
audience: Option<&str>,
scopes: Option<&str>,
insecure: bool,
)
| 100 | /// Opens the user's browser to the Keycloak login page and waits for |
| 101 | /// the authorization code redirect on a localhost callback server. |
| 102 | pub async fn oidc_browser_auth_flow( |
| 103 | issuer: &str, |
| 104 | client_id: &str, |
| 105 | audience: Option<&str>, |
| 106 | scopes: Option<&str>, |
| 107 | insecure: bool, |
| 108 | ) -> Result<OidcTokenBundle> { |
| 109 | let discovery = discover(issuer, insecure).await?; |
| 110 | |
| 111 | let listener = TcpListener::bind("127.0.0.1:0").await.into_diagnostic()?; |
| 112 | let port = listener.local_addr().into_diagnostic()?.port(); |
| 113 | let redirect_uri = format!("http://127.0.0.1:{port}/callback"); |
| 114 | |
| 115 | let client = BasicClient::new(ClientId::new(client_id.to_string())) |
| 116 | .set_auth_uri(AuthUrl::new(discovery.authorization_endpoint).into_diagnostic()?) |
| 117 | .set_token_uri(TokenUrl::new(discovery.token_endpoint).into_diagnostic()?) |
| 118 | .set_redirect_uri(RedirectUrl::new(redirect_uri).into_diagnostic()?); |
| 119 | |
| 120 | let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); |
| 121 | |
| 122 | let mut auth_request = client |
| 123 | .authorize_url(CsrfToken::new_random) |
| 124 | .set_pkce_challenge(pkce_challenge); |
| 125 | |
| 126 | for scope in build_scopes(scopes) { |
| 127 | auth_request = auth_request.add_scope(scope); |
| 128 | } |
| 129 | |
| 130 | let (mut auth_url, csrf_token) = auth_request.url(); |
| 131 | |
| 132 | // Append audience parameter for providers like Entra ID where the API |
| 133 | // audience differs from the client ID. |
| 134 | if let Some(aud) = audience { |
| 135 | auth_url.query_pairs_mut().append_pair("audience", aud); |
| 136 | } |
| 137 | |
| 138 | let (tx, rx) = oneshot::channel::<String>(); |
| 139 | let expected_state = csrf_token.secret().clone(); |
| 140 | |
| 141 | let server_handle = tokio::spawn(run_oidc_callback_server(listener, tx, expected_state)); |
| 142 | |
| 143 | eprintln!(" Opening browser for OIDC authentication..."); |
| 144 | if let Err(e) = crate::auth::open_browser_url(auth_url.as_str()) { |
| 145 | debug!(error = %e, "failed to open browser"); |
| 146 | eprintln!("Could not open browser automatically."); |
| 147 | eprintln!("Open this URL in your browser:"); |
| 148 | eprintln!(" {auth_url}"); |
| 149 | eprintln!(); |
| 150 | } else { |
| 151 | eprintln!(" Browser opened. Waiting for authentication..."); |
| 152 | } |
| 153 | |
| 154 | let code = tokio::select! { |
| 155 | result = rx => { |
| 156 | result.map_err(|_| miette::miette!("OIDC callback channel closed unexpectedly"))? |
| 157 | } |
| 158 | () = tokio::time::sleep(AUTH_TIMEOUT) => { |
| 159 | return Err(miette::miette!( |
no test coverage detected