Manipulando senhas antes de efetuar o login no Azure AD B2C (Password Salt)

Introdução

Nesse post, abordarei um método polêmico, pois o envio de senhas através de requisições não é recomendado, mas às vezes é a única saída que você encontrará.

Imagine que você está migrando seus usuários para o Azure AD B2C. Todas as suas senhas são criptografadas em SHA-256, e a gestão do projeto não quer que os usuários efetuem a troca de senha. O tipo de criptografia não importa, desde que o hash nunca altere. Caso a criptografia que seu projeto use seja reversível, também não há necessidade de seguir esse post. Você pode simplesmente descriptografar as senhas e importá-las direto (seria até o ideal).

Levando em consideração que sua criptografia é irreversível e imutável (a menos que o usuário defina uma nova senha), no momento que você começou a popular o seu Azure AD B2C, você não tinha controle nenhum de qual era a senha real do seu usuário. Foi algo parecido com isso:

Exemplo da tabela no banco de dados

Então quando você criou o seu usuário foi algo parecido com:

1
var graphNewUser =
2
new User
3
{
4
Identities =
5
new List<ObjectIdentity>()
6
{
7
new ObjectIdentity()
8
{
9
SignInType = "emailAddress",
10
Issuer = AzureAdB2CTenant,
11
IssuerAssignedId = "julianobiffi@hotmail.com"
12
}
13
},
14
PasswordProfile =
15
new PasswordProfile
16
{
17
ForceChangePasswordNextSignIn = false,
18
Password = "157edfef911735f5446d87c1889e47a6b6e34b3be4caf511b1dbf2d74fae7117"
19
},
20
PasswordPolicies = "DisablePasswordExpiration,DisableStrongPassword",
21
//Other properties
22
};
23
24
await _GraphApiService.GraphServiceClient
25
.Users
26
.PostAsync(graphNewUser);

Partindo disso, quando o usuário tentar logar com sua senha techbiffi123, não conseguirá, pois, para o Azure AD B2C, sua senha é 157edfef911735f5446d87c1889e47a6b6e34b3be4caf511b1dbf2d74fae7117. Daí surge essa solução: iremos interceptar a senha do usuário, aplicar o hash do seu projeto legado e tentar fazer o login com essa senha com hash usando as custom policies e utilizando o SocialAndLocalAccounts.

Implementação

Criação da ClaimType

Para o controle e a utilização da senha com o hash no decorrer do fluxo, será necessária a definição de um ClaimType que fica dentro da ClaimsSchema.

TrustFrameworkBase.xml
1
<ClaimsSchema>
2
<!-- Others Claim types -->
3
4
<ClaimType Id="passwordWithEncrypt">
5
<DisplayName>passwordWithEncrypt</DisplayName>
6
<DataType>string</DataType>
7
<UserHelpText>Defines the user password with my legacy hash applied.</UserHelpText>
8
</ClaimType>
9
</ClaimsSchema>

Criação dos perfis técnicos

O primeiro perfil técnico que será necessário definir é o perfil técnico RESTful para enviar a senha que o usuário inseriu, enviá-la para sua API e receber a senha com o hash aplicado.

TrustFrameworkBase.xml
1
<ClaimsProvider>
2
<Domain>Password Encription</Domain>
3
<DisplayName>Request To encrypt the password</DisplayName>
4
<TechnicalProfiles>
5
<TechnicalProfile Id="REST-PasswordEncription">
6
<DisplayName>Apply the legacy encryption into the provided password</DisplayName>
7
<Protocol Name="Proprietary"
8
Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
9
<Metadata>
10
<Item Key="ServiceUrl">
11
{Settings:MyApiUrl}/EncryptPassword</Item>
12
<Item Key="SendClaimsIn">Body</Item>
13
<Item Key="AuthenticationType">ApiKeyHeader</Item>
14
</Metadata>
15
<CryptographicKeys>
16
<Key Id="x-functions-key" StorageReferenceId="B2C_1A_RestApiKey" />
17
</CryptographicKeys>
18
<InputClaims>
19
<InputClaim ClaimTypeReferenceId="password" />
20
</InputClaims>
21
<OutputClaims>
22
<OutputClaim ClaimTypeReferenceId="passwordWithEncrypt" PartnerClaimType="passwordWithEncrypt" />
23
</OutputClaims>
24
</TechnicalProfile>
25
</TechnicalProfiles>
26
</ClaimsProvider>

Agora você precisará duplicar o perfil técnico login-NonInteractive, perfil responsável pelo login do usuário. Após duplicar, altere a InputClaim de password, apontando para o ClaimTypeReferenceId passwordWithEncrypt.

TrustFrameworkBase.xml
1
<ClaimsProvider>
2
<DisplayName>Local Account Sign In With Password Encryption</DisplayName>
3
<TechnicalProfiles>
4
<TechnicalProfile Id="login-NonInteractive-password-encryption">
5
<DisplayName>Local Account Sign In With Password Encryption</DisplayName>
6
<Protocol Name="OpenIdConnect" />
7
<Metadata>
8
<Item Key="ProviderName">https://sts.windows.net/</Item>
9
<Item Key="METADATA">https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration</Item>
10
<Item Key="authorization_endpoint">https://login.microsoftonline.com/{tenant}/oauth2/token</Item>
11
<Item Key="response_types">id_token</Item>
12
<Item Key="response_mode">query</Item>
13
<Item Key="scope">email openid</Item>
14
<!-- <Item Key="grant_type">password</Item> -->
15
16
<!-- Policy Engine Clients -->
17
<Item Key="UsePolicyInRedirectUri">false</Item>
18
<Item Key="HttpBinding">POST</Item>
19
</Metadata>
20
<InputClaims>
21
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
22
<InputClaim ClaimTypeReferenceId="passwordWithEncrypt" PartnerClaimType="password" Required="true" />
23
<InputClaim ClaimTypeReferenceId="grant_type" DefaultValue="password" AlwaysUseDefaultValue="true" />
24
<InputClaim ClaimTypeReferenceId="scope" DefaultValue="openid" AlwaysUseDefaultValue="true" />
25
<InputClaim ClaimTypeReferenceId="nca" PartnerClaimType="nca" DefaultValue="1" />
26
</InputClaims>
27
<OutputClaims>
28
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid" />
29
<OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
30
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
31
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
32
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
33
<OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
34
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
35
</OutputClaims>
36
</TechnicalProfile>
37
</TechnicalProfiles>
38
</ClaimsProvider>

Seguindo, precisaremos incluir ambos os perfis no perfil técnico no fluxo de login, que é feito através do SelfAsserted-LocalAccountSignin-Email. Iremos adicioná-los dentro do ValidationTechnicalProfiles. Note que adicionei o fluxo de senha com criptografia antes do login convencional. Em um cenário onde o usuário já redefiniu a senha no portal do Azure, ele não precisaria passar pelo fluxo de criptografia; porém, ele ainda assim irá passar, aumentando o tempo na jornada de login. O que podemos fazer é criar um atributo customizado para definir se ele já redefiniu a senha pelo Azure AD B2C e evitar que ele passe por esse fluxo caso erre a senha. Contudo, isso fica para outro post.

TrustFrameworkBase.xml
1
<ValidationTechnicalProfiles>
2
<!-- Login flow with legacy encryption -->
3
<ValidationTechnicalProfile ReferenceId="REST-PasswordEncription" ContinueOnError="true" />
4
<ValidationTechnicalProfile ReferenceId="login-NonInteractive-password-encryption" ContinueOnError="true" ContinueOnSuccess="false">
5
<Preconditions>
6
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
7
<Value>passwordWithEncrypt</Value>
8
<Action>SkipThisValidationTechnicalProfile</Action>
9
</Precondition>
10
</Preconditions>
11
</ValidationTechnicalProfile>
12
13
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" ContinueOnError="true" ContinueOnSuccess="false"/>
14
</ValidationTechnicalProfiles>

A utilização do ContinueOnError="true" e do ContinueOnSuccess="false" é necessária, pois, sem o ContinueOnError="true", o perfil de validação já retornaria erro (em cenários onde o usuário já redefiniu a senha ou a sua API retornar algum erro) e não continuaria. Mas não se preocupe, caso os três perfis deem erro, o usuário receberá a mensagem de que a senha informada está errada.

Como duplicamos o perfil técnico do login-NonInteractive e o transformamos no login-NonInteractive-password-encryption, será necessário também incluir as informações de client_id, IdTokenAudience e resource_id, que ficam no TrustFrameworkExtensions.xml.

TrustFrameworkExtensions.xml
1
<ClaimsProvider>
2
<DisplayName>Local Account Sign In With Password Encryption</DisplayName>
3
<TechnicalProfiles>
4
<TechnicalProfile Id="login-NonInteractive-password-encryption">
5
<Metadata>
6
<Item Key="client_id">{Settings:ProxyIdentityExperienceFrameworkClientId}</Item>
7
<Item Key="IdTokenAudience">{Settings:IdentityExperienceFrameworkClientId}</Item>
8
</Metadata>
9
<InputClaims>
10
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="{Settings:ProxyIdentityExperienceFrameworkClientId}" />
11
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="{Settings:IdentityExperienceFrameworkClientId}" />
12
</InputClaims>
13
</TechnicalProfile>
14
</TechnicalProfiles>
15
</ClaimsProvider>

Tudo pronto agora, basta compilar suas políticas e fazer o upload. Algo importante a ser reforçado é que o Azure AD B2C costuma ter um delay de até 30 minutos na propagação de novas políticas. Então, não se assuste caso você faça o upload e nada aconteça imediatamente. Uma prática que me ajuda bastante na hora de testar é deletar a TrustFrameworkBase.xml e esperar a propagação na minha SignUpOrSignin. Quando ela começa a dar erro, faço o upload da nova TrustFrameworkBase.xml e realizo os testes.

Abaixo, demonstro o fluxo do login. Inicialmente, com a senha "original" populada no Azure AD B2C do meu usuário (o login é realizado através do fluxo convencional). Em seguida, refaço o login, agora com a senha "real" do usuário (o login é realizado pelo fluxo de criptografia de senha, onde envio a senha, recebo a nova senha e efetuo o login com ela). Example of login using real password and populed password

Caso tenha se perdido em algum momento, recomendo que volte e siga os passos com atenção. Se achar mais fácil, vou deixar o meu repositório do B2C do projeto, onde as alterações foram feitas no commit fc43d32.