-
Notifications
You must be signed in to change notification settings - Fork 134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support for migrating totp #197
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -84,6 +84,12 @@ The GET request will have to return user data as a JSON response in the form: | |
], | ||
"requiredActions": [ | ||
"requiredActions" | ||
], | ||
"totps": [ | ||
{ | ||
"name": "string", | ||
"secret": "string" | ||
} | ||
] | ||
} | ||
``` | ||
|
@@ -143,6 +149,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" | ||
} | ||
] | ||
} | ||
``` | ||
|
@@ -296,3 +312,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 looks 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 the utf8 bytes. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using " There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Grammar nitpick: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I understand it correctly, but wouldn't |
||
For the utf8 bytes just set the encoding attribute to null. | ||
Possible Algorithms 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 |
---|---|---|
@@ -0,0 +1,64 @@ | ||
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()); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double newline |
||
|
||
@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 |
---|---|---|
@@ -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"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of the properties are missing here.