Skip to content

Commit

Permalink
Merge pull request #197 from Jonbeckas/migrate-totp
Browse files Browse the repository at this point in the history
feat: add support for migrating totp
  • Loading branch information
daniel-frak authored Sep 26, 2024
2 parents d9544d6 + 581a36f commit 374a4f5
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 5 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ The GET request will have to return user data as a JSON response in the form:
],
"requiredActions": [
"requiredActions"
],
"totps": [
{
"name": "string",
"secret": "string",
"digits": "int",
"period": "int",
"algorithm": "string",
"encoding": "string"
}
]
}
```
Expand Down Expand Up @@ -143,6 +153,16 @@ response might look like this:
"UPDATE_PASSWORD",
"UPDATE_PROFILE",
"update_user_locale"
],
"totps": [
{
"name": "Totp Device 1",
"secret": "secret",
"digits": 6,
"period": 30,
"algorithm": "HmacSHA1",
"encoding": "BASE32"
}
]
}
```
Expand Down Expand Up @@ -296,3 +316,19 @@ automatically map legacy groups to Keycloak groups, by specifying the mapping in

This switch can be toggled to decide whether groups which are not defined in the legacy group conversion map should be
migrated anyway or simply ignored.

## Totp
This module supports the migration of totp devices. The totp configuration block could look like this:
```json
{
"name": "Totp Device 1",
"secret": "secret",
"digits": 6,
"period": 30,
"algorithm": "HmacSHA1",
"encoding": "BASE32"
}
```
`name` should be the name of the totp device, while `secret` is the secret, that could be encoded in "BASE32" or as UTF-8 plaintext.
For the utf8 bytes just set the `encoding` attribute to null.
Possible `algorithm`s are: HmacSHA1, HmacSHA256, HmacSHA512
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.danielfrak.code.keycloak.providers.rest.remote;

import java.util.Objects;

public class LegacyTotp {

private String secret;
private String name;
private int digits;
private int period;
private String algorithm;
private String encoding;

public String getSecret() {
return this.secret;
}

public void setSecret(String secret) {
this.secret = secret;
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public int getDigits() {
return this.digits;
}

public void setDigits(int digits) {
this.digits = digits;
}

public int getPeriod() {
return this.period;
}

public void setPeriod(int period) {
this.period = period;
}

public String getAlgorithm() {
return this.algorithm;
}

public void setAlgorithm(String algorith) {
this.algorithm = algorith;
}

public String getEncoding() {
return this.encoding;
}

public void setEncoding(String encoding) {
this.encoding = encoding;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LegacyTotp legacyTotp = (LegacyTotp) o;

return Objects.equals(secret, legacyTotp.secret) &&
Objects.equals(name, legacyTotp.name) &&
Objects.equals(algorithm, legacyTotp.algorithm) &&
Objects.equals(encoding, legacyTotp.encoding) &&
digits == legacyTotp.digits &&
period == legacyTotp.period;
}

@Override
public int hashCode() {
return Objects.hash(name, secret, digits, period, algorithm, encoding);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class LegacyUser {
private List<String> roles;
private List<String> groups;
private List<String> requiredActions;
private List<LegacyTotp> totps;

public String getId() {
return id;
Expand Down Expand Up @@ -109,6 +110,14 @@ public void setRequiredActions(List<String> requiredActions) {
this.requiredActions = requiredActions;
}

public List<LegacyTotp> getTotps() {
return this.totps;
}

public void setTotps(List<LegacyTotp> totps) {
this.totps = totps;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -129,12 +138,13 @@ public boolean equals(Object o) {
Objects.equals(attributes, legacyUser.attributes) &&
Objects.equals(roles, legacyUser.roles) &&
Objects.equals(groups, legacyUser.groups) &&
Objects.equals(requiredActions, legacyUser.requiredActions);
Objects.equals(requiredActions, legacyUser.requiredActions) &&
Objects.equals(totps, legacyUser.totps);
}

@Override
public int hashCode() {
return Objects.hash(id, username, email, firstName, lastName, isEnabled, isEmailVerified, attributes,
roles, groups, requiredActions);
roles, groups, requiredActions, totps);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.*;
import org.keycloak.models.credential.OTPCredentialModel;

import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -93,6 +94,19 @@ public UserModel create(LegacyUser legacyUser, RealmModel realm) {
.forEach(userModel::addRequiredAction);
}

if (legacyUser.getTotps() != null) {
legacyUser.getTotps().forEach(totp -> {
var otpModel = OTPCredentialModel.createTOTP(
totp.getSecret(),
totp.getDigits(),
totp.getPeriod(),
totp.getAlgorithm(),
totp.getEncoding());
otpModel.setUserLabel(totp.getName());
userModel.credentialManager().createStoredCredential(otpModel);
});
}

return userModel;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.danielfrak.code.keycloak.providers.rest.remote;

import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class LegacyTotpTest {

@Test
void shouldGetAndSetName() {
var totp = new LegacyTotp();
var expectedValue = "value1";
totp.setName(expectedValue);
assertEquals(expectedValue, totp.getName());
}

@Test
void shouldGetAndSetSecret() {
var totp = new LegacyTotp();
var expectedValue = "value1";
totp.setSecret(expectedValue);
assertEquals(expectedValue, totp.getSecret());
}

@Test
void shouldGetAndSetDigits() {
var totp = new LegacyTotp();
var expectedValue = 6;
totp.setDigits(expectedValue);
assertEquals(expectedValue, totp.getDigits());
}

@Test
void shouldGetAndSetPeriod() {
var totp = new LegacyTotp();
var expectedValue = 30;
totp.setPeriod(expectedValue);
assertEquals(expectedValue, totp.getPeriod());
}

@Test
void shouldGetAndSetAlgorithm() {
var totp = new LegacyTotp();
var expectedValue = "value1";
totp.setAlgorithm(expectedValue);
assertEquals(expectedValue, totp.getAlgorithm());
}

@Test
void shouldGetAndSetEncpding() {
var totp = new LegacyTotp();
var expectedValue = "value1";
totp.setEncoding(expectedValue);
assertEquals(expectedValue, totp.getEncoding());
}

@Test
void testEquals() {
EqualsVerifier.simple().forClass(LegacyTotp.class)
.verify();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ void shouldGetAndSetRequiredActions() {
assertEquals(expectedValue, user.getRequiredActions());
}

@Test
void shouldGetAndSetTotps() {
var user = new LegacyUser();
var legacyTotp = new LegacyTotp();
legacyTotp.setName("value1");
legacyTotp.setSecret("value2");
var expectedValue = singletonList(legacyTotp);
user.setTotps(expectedValue);
assertEquals(expectedValue, user.getTotps());
}

@Test
void testEquals() {
EqualsVerifier.simple().forClass(LegacyUser.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.danielfrak.code.keycloak.providers.rest.remote;

import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.SubjectCredentialManager;

import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

public class TestCredentialManager implements SubjectCredentialManager {

private final Set<CredentialModel> storedCredentials = new HashSet<>();

@Override
public boolean isValid(List<CredentialInput> inputs) {
throw new RuntimeException("Not implemented");
}

@Override
public boolean updateCredential(CredentialInput input) {
throw new RuntimeException("Not implemented");
}

@Override
public void updateStoredCredential(CredentialModel cred) {
throw new RuntimeException("Not implemented");
}

@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
this.storedCredentials.add(cred);
return cred;
}

@Override
public boolean removeStoredCredentialById(String id) {
throw new RuntimeException("Not implemented");
}

@Override
public CredentialModel getStoredCredentialById(String id) {
throw new RuntimeException("Not implemented");
}

@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
throw new RuntimeException("Not implemented");
}

@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return this.storedCredentials.stream().filter(credentialModel -> Objects.equals(credentialModel.getType(), type));
}

@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
throw new RuntimeException("Not implemented");
}

@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
throw new RuntimeException("Not implemented");
}

@Override
public void updateCredentialLabel(String credentialId, String credentialLabel) {
throw new RuntimeException("Not implemented");
}

@Override
public void disableCredentialType(String credentialType) {
throw new RuntimeException("Not implemented");
}

@Override
public Stream<String> getDisableableCredentialTypesStream() {
throw new RuntimeException("Not implemented");
}

@Override
public boolean isConfiguredFor(String type) {
throw new RuntimeException("Not implemented");
}

@Override
public boolean isConfiguredLocally(String type) {
throw new RuntimeException("Not implemented");
}

@Override
public Stream<String> getConfiguredUserStorageCredentialTypesStream() {
throw new RuntimeException("Not implemented");
}

@Override
public CredentialModel createCredentialThroughProvider(CredentialModel model) {
throw new RuntimeException("Not implemented");
}
}
Loading

0 comments on commit 374a4f5

Please sign in to comment.