Login no app .NET MAUI passo a passo: WebAuthenticator, PKCE e token guardado
Implemente o login de um app MAUI pelo navegador do sistema: gere PKCE e state, registre o retorno no Android e no iOS, troque o código por token e guarde no SecureStorage.
O app de vendas precisa de login com a conta corporativa. A primeira versão abriu um WebView com a página do provedor dentro do app, e o provedor começou a bloquear com "este navegador não é seguro". A resposta curta: em .NET MAUI, o login vai pelo WebAuthenticator, que abre o navegador do sistema, e o app recebe de volta só um código, que troca por token usando PKCE. Nenhum segredo de cliente no app, nenhuma senha digitada em tela sua.
Por que o WebView é recusado está explicado em o autenticador do MAUI não é uma WebView sua, e as regras de redirect do OAuth nativo em OAuth no aplicativo nativo. Este guia é a implementação, do começo ao fim, para o app Vendas, com client id app-vendas e retorno br.exemplo.vendas://callback.
Como o fluxo funciona
- O app gera dois valores aleatórios: o
code_verifier(fica guardado em memória) e ostate. - Calcula o
code_challenge, que é o SHA-256 do verifier em Base64 URL. - Abre o navegador do sistema na página de autorização do provedor, mandando o challenge e o state.
- A pessoa entra no domínio real do provedor, com gerenciador de senhas e segundo fator funcionando.
- O provedor redireciona para
br.exemplo.vendas://callback?code=...&state=..., e o sistema entrega esse endereço ao app. - O app confere o state e troca o código por token, mandando o verifier original.
O PKCE resolve um problema específico do celular: outro app pode registrar o mesmo esquema de URL e receber o código. Sem o verifier, que nunca saiu do seu app, esse código não vale nada. O state impede que um retorno forjado, iniciado por outra pessoa, seja aceito como se fosse do seu login.
Como gerar PKCE e state
using System.Security.Cryptography;
using System.Text;
public static class Pkce
{
public static string Aleatorio(int bytes = 32) =>
Base64Url(RandomNumberGenerator.GetBytes(bytes));
public static string Desafio(string verifier) =>
Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
static string Base64Url(byte[] dados) =>
Convert.ToBase64String(dados).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
Use RandomNumberGenerator, não Random. Um verifier previsível anula o PKCE.
Como chamar o WebAuthenticator e trocar o código
using System.Net.Http.Json;
public record Tokens(string access_token, string? refresh_token, int expires_in);
public class Login
{
const string Retorno = "br.exemplo.vendas://callback";
readonly HttpClient http = new();
public async Task<bool> EntrarAsync()
{
var verifier = Pkce.Aleatorio();
var state = Pkce.Aleatorio(16);
var url = "https://login.exemplo.com/authorize"
+ "?response_type=code"
+ "&client_id=app-vendas"
+ "&redirect_uri=" + Uri.EscapeDataString(Retorno)
+ "&scope=" + Uri.EscapeDataString("openid profile api-pedidos")
+ "&code_challenge=" + Pkce.Desafio(verifier)
+ "&code_challenge_method=S256"
+ "&state=" + state;
WebAuthenticatorResult resultado;
try
{
resultado = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(url), new Uri(Retorno));
}
catch (TaskCanceledException)
{
return false; // a pessoa fechou o navegador
}
if (!resultado.Properties.TryGetValue("state", out var voltou) || voltou != state)
throw new InvalidOperationException("state não confere");
var code = resultado.Properties["code"];
var resposta = await http.PostAsync("https://login.exemplo.com/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code"] = code,
["redirect_uri"] = Retorno,
["client_id"] = "app-vendas",
["code_verifier"] = verifier
}));
resposta.EnsureSuccessStatusCode();
var tokens = await resposta.Content.ReadFromJsonAsync<Tokens>()
?? throw new InvalidOperationException("resposta vazia");
await SecureStorage.Default.SetAsync("access_token", tokens.access_token);
if (tokens.refresh_token is not null)
await SecureStorage.Default.SetAsync("refresh_token", tokens.refresh_token);
return true;
}
}
Os endereços /authorize e /token são fictícios; os do seu provedor estão no documento de descoberta dele (/.well-known/openid-configuration). Repare no que não aparece: client_secret. No provedor, o app-vendas precisa estar cadastrado como cliente público, com PKCE e com exatamente esse redirect na lista.
Como registrar o retorno em cada plataforma
O sistema só entrega br.exemplo.vendas://callback ao app se o app disser que atende esse esquema. Use um esquema em formato de domínio invertido, que é muito menos provável de colidir com outro app do que myapp://.
No Android, crie uma Activity de retorno em Platforms/Android:
using Android.App;
using Android.Content;
using Android.Content.PM;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "br.exemplo.vendas")]
public class RetornoDoLogin : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity
{
}
A documentação do MAUI também pede, para Android 11 em diante, uma entrada queries no manifesto para que o app enxergue o serviço de Custom Tabs do navegador:
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
No iOS, registre o esquema em CFBundleURLTypes no Info.plist, conforme a página de autenticação do MAUI. No Windows, confira o suporte atual na mesma documentação antes de prometer o recurso.
Depois do login
O token está no SecureStorage. Toda chamada à API leva esse token, e a API valida assinatura, emissor, audiência e prazo. Se o app usa Blazor Hybrid, o próximo passo é avisar a interface de quem entrou, como mostra Blazor Hybrid com login. No "sair", apague os dois tokens e, se o provedor oferecer, chame o endpoint de revogação.
O access token costuma durar minutos ou poucas horas. Quando a API responder 401, use o refresh token no mesmo endpoint /token, com grant_type=refresh_token, e grave o par novo. Se a renovação também falhar, apague tudo e chame EntrarAsync de novo. Não peça a senha em tela própria como plano B: o caminho de volta é sempre o navegador.
Como testar o fluxo antes de publicar
Quatro testes pegam quase todos os problemas. Primeiro, o caminho feliz num aparelho real, não só no emulador: o navegador abre, você entra e o app volta sozinho para a tela inicial. Segundo, feche o navegador no meio do login: o app precisa voltar à tela de entrada sem travar e sem mensagem de erro técnico. Terceiro, altere o state de propósito num build de depuração: o login tem de ser recusado. Quarto, desinstale e reinstale o app. No iOS, itens do Keychain podem sobreviver à desinstalação; se você não quer que a conta anterior volte sozinha, grave em Preferences uma marca de "primeira execução" e, quando ela não existir, chame SecureStorage.Default.RemoveAll() antes de tudo.
Erros comuns
- Login em WebView com o formulário do provedor. Provedores grandes bloqueiam, e a pessoa não tem como confirmar que está no domínio verdadeiro.
- Client secret no app. Qualquer um extrai do pacote e se passa pelo seu cliente.
- Ignorar o state. Um retorno forjado é aceito e a pessoa entra na conta de outra, sem perceber.
- Esquema genérico como
myapp://. Colide com outro app instalado, e o código vai parar no lugar errado. Com PKCE ele não serve sozinho, mas o login falha. - Redirect diferente entre o app e o provedor. Uma barra no fim ou uma letra maiúscula, e o provedor recusa com "redirect_uri mismatch".
- Token em
Preferences. Vira texto em arquivo de configuração, legível em backup ou aparelho com root.
Checklist
- Cliente cadastrado como público, com PKCE e um único redirect.
- Verifier e state gerados com
RandomNumberGeneratora cada login. - State conferido antes de usar o código.
- Activity de retorno no Android com o esquema certo; esquema registrado no iOS.
- Nenhum
client_secretno projeto. - Tokens no
SecureStoragee apagados no "sair". - Cancelamento do navegador tratado sem mensagem de erro.
Abra o login no seu app agora e olhe o topo da tela: aparece a barra do navegador com o domínio do provedor? Se aparece o visual do seu app sem endereço nenhum, você ainda está num WebView. E a pergunta para conferir no painel do provedor: quantas URLs de redirect estão cadastradas para esse cliente, e todas são suas?
Referência: https://learn.microsoft.com/dotnet/maui/platform-integration/communication/authentication