Exigimos MFA. Um parâmetro de query a desligou.
Lançamos a autenticação multifator. Depois da senha, a página de login desafiava por um segundo fator, do jeito que você esperaria. Testamos, funcionou, seguimos em frente. Então, numa revisão pré-lançamento do nosso próprio servidor de auth, descobrimos que dava para pular a coisa toda editando uma URL.
Eis o formato da coisa. Num fluxo OIDC, sua aplicação envia o usuário para /connect/authorize. Se ele ainda não está autenticado, o servidor o redireciona para a página de login com o destino original guardado num parâmetro: /login?returnUrl=/connect/authorize?.... Você digita sua senha, passa pela MFA, e o fluxo de login devolve você àquele returnUrl, e nesse ponto /connect/authorize emite um código de autorização e os tokens vêm em seguida.
O bug morava na ordem das operações. A etapa da senha autenticava você. Ou seja, definia um cookie autenticado. A MFA era a página seguinte na sequência. E /connect/authorize, o endpoint que de fato entrega os tokens, fazia exatamente uma pergunta antes de entregá-los: esta requisição está autenticada? Nunca perguntava se a sessão tinha passado por um segundo fator. "Autenticado" e "autenticado com MFA" eram o mesmo cookie.
Então o bypass não tem nada de engenhoso. Você envia sua senha. Recebe o cookie autenticado. Aí, em vez de seguir o fluxo até o prompt de MFA, você vai direto ao returnUrl por conta própria, /connect/authorize?..., o endereço que o servidor te entregou logo no início. O authorize vê um principal autenticado, roda sua única verificação e emite o código. O segundo fator nunca acontece. O prompt de MFA era uma etapa num corredor, e nada impedia você de dar a volta nela.
A razão de isso sobreviver aos testes é que o caminho feliz é impecável. Clique pela interface como uma pessoa e você será desafiado toda vez. O portão é real. Só está no lugar errado. A fronteira de segurança num servidor OIDC não é a tela de login. É /connect/authorize, o ponto onde uma sessão vira tokens. Qualquer requisito que viva apenas nas páginas que levam até essa fronteira é apenas uma recomendação, porque qualquer um que consiga fazer a requisição subjacente pode fazê-la diretamente.
Essa mudança de enquadramento é a correção. MFA não é uma etapa que você executa. É uma propriedade que a sessão tem ou não tem. Então paramos de modelá-la como uma página e passamos a modelá-la como uma claim. Quando você passa pelo segundo fator, o fluxo de login grava um marcador mfa_authenticated no cookie de auth. /connect/authorize não aceita mais "autenticado". Exige "autenticado e portando esse marcador". Um cookie só de senha agora é inerte no token endpoint: ele é devolvido para terminar a MFA não importa para qual URL você o aponte, porque o que lhe falta é uma claim, não uma visita a uma página.
A federação se encaixa nisso de forma limpa. Quando um usuário entra por um provedor de identidade externo que rodou sua própria MFA, esse provedor atesta o segundo fator, então o sign-in federado define o mesmo marcador. Usuários de SSO não são desafiados duas vezes por algo que o IdP deles já impôs. O marcador é a única fonte de verdade, e todo caminho que pode legitimamente satisfazer a MFA o escreve.
A lição transferível não tem nada a ver com MFA em particular. É esta: imponha um controle na fronteira que concede a coisa que você está protegendo, não na etapa de interface feita para levar até lá. Nós tínhamos a verificação. Tínhamos escrito a lógica de MFA, testado, lançado. Ela só estava presa à parte do sistema que um atacante não precisa usar. Telas de consentimento, aceite de termos, step-up para escopos sensíveis: mesmo modo de falha. Se o endpoint que emite a credencial não verifica a condição, a tela que a pede é uma sugestão.
Pegamos esse antes de qualquer cliente esbarrar nele, na mesma auditoria que revelou alguns de seus irmãos. Esse é o argumento para revisar seu próprio auth como se você o estivesse atacando, e especificamente para perguntar, em cada endpoint que entrega algo valioso, não "o usuário percorreu o fluxo?" mas "o que esta requisição realmente prova?". A nossa provava uma senha. Estávamos tratando isso como prova de dois fatores.
Impor MFA no endpoint que cunha os tokens, não na página que a pede, é toda a diferença entre MFA de verdade e uma sugestão. O Authagonal impõe isso onde a credencial é emitida, então uma URL de retorno editada à mão não leva a lugar nenhum.