Skip to content

Commit

Permalink
Story #12364: improve saml documentation & #13112: upgrade external p…
Browse files Browse the repository at this point in the history
…roviders (#1944)

* Story#12364: improve saml integration documentation

* Story#13112: handle WantsAssertionsSigned and AuthnRequestSigned default values and migration scripts

---------

Co-authored-by: Benaissa BENARBIA <benaissa.benarbia.ext@culture.gouv.fr>
  • Loading branch information
bbenaissa and Benaissa BENARBIA authored Jul 15, 2024
1 parent d97066e commit 45954b5
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public IdentityProviderDto mapToIdentityProviderDto(
dto.getIdentifierAttribute(),
dto.getMaximumAuthenticationLifetime(),
dto.getAuthnRequestBinding(),
Objects.isNull(dto.getWantsAssertionsSigned()) ? false : dto.getWantsAssertionsSigned(),
Objects.isNull(dto.getAuthnRequestSigned()) ? false : dto.getAuthnRequestSigned(),
Objects.isNull(dto.getWantsAssertionsSigned()) ? true : dto.getWantsAssertionsSigned(),
Objects.isNull(dto.getAuthnRequestSigned()) ? true : dto.getAuthnRequestSigned(),
dto.isPropagateLogout(),
dto.isAutoProvisioningEnabled(),
dto.getClientId(),
Expand Down
4 changes: 2 additions & 2 deletions cas/cas-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ To set up Saml V2 authentication with vitamui, please follow these steps:
keytool -importcert -keystore environments/keystores/server/my_server/keystore_cas-server.jks -storepass xxx -alias orga-saml -file environments/certs/orga-SAML.crt
```
- In Vitamui interface, we create an external provider of SAML type with the following informations:
- Email attribute: keep it empty, except if the idp provide the email after authentication
- Email attribute: The attribute containing the user email sent by the idp after authentication, please check that the attribute 'nameid-format' to 'emailAddress' instead of 'transient'
- Upload the CAS keystore file (with the associated password) (keystore_cas-server.jks)
- Upload the IDP metadata file (e.g., FederationMetadata.xml)
- After provider creation, we need to download the metadata file of the vitamui provider (spmetadata.xml), and
Expand Down Expand Up @@ -347,7 +347,7 @@ To test the saml authentication, bellow an example with an external CAS on versi
- Login in into Vitamui as a superadmin, create a SAML provider with the following information:
- Pattern: email domain configured before: ```mydomainmail.fr```
- Type: ```SAML```
- Email attribute: empty (except if the cas send the email after authentication)
- Email attribute: The attribute containing the user email sent by the idp after authentication, please check that the attribute 'nameid-format' to 'emailAddress' instead of 'transient'
- CAS Keystore: for testing: you upload any keystore with the right passowrd.
- IDP Metadata: upload the file generated by running external CAS, from the path ```/etc/cas/saml/idp-metadata.xml```
- Assertions : false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto;
import org.pac4j.core.client.IndirectClient;

import java.util.Objects;

/**
* Pac4j client identity provider.
*
Expand All @@ -61,7 +63,7 @@ public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final Indir
setIdentifierAttribute(dto.getIdentifierAttribute());
setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled());
setProtocoleType(dto.getProtocoleType());
setPropagateLogout(dto.isPropagateLogout());
setPropagateLogout(Objects.isNull(dto.isPropagateLogout()) ? false : dto.isPropagateLogout());

setKeystoreBase64(dto.getKeystoreBase64());
setKeystorePassword(dto.getKeystorePassword());
Expand All @@ -70,8 +72,10 @@ public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final Indir
setSpMetadata(dto.getSpMetadata());
setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime());
setAuthnRequestBinding(dto.getAuthnRequestBinding());
setWantsAssertionsSigned(dto.getWantsAssertionsSigned());
setAuthnRequestSigned(dto.getAuthnRequestSigned());
setWantsAssertionsSigned(
Objects.isNull(dto.getWantsAssertionsSigned()) ? true : dto.getWantsAssertionsSigned()
);
setAuthnRequestSigned(Objects.isNull(dto.getAuthnRequestSigned()) ? true : dto.getAuthnRequestSigned());

setClientId(dto.getClientId());
setClientSecret(dto.getClientSecret());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
print("START 61_update_existing_external_providers_models.js.j2");

db = db.getSiblingDB('iam');

db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
wantsAssertionsSigned: { $exists: false }
},
[
{
$set: {
wantsAssertionsSigned: {
$cond: {
if: { $eq: [{ $type: "$spMetadata" }, "missing"] },
then: false,
else: {
$cond: {
if: { $regexMatch: { input: "$spMetadata", regex: /WantAssertionsSigned="true"/ } },
then: true,
else: false
}
}
}
}
}
}
]
);



db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
authnRequestSigned: { $exists: false }
},
[
{
$set: {
authnRequestSigned: {
$cond: {
if: { $eq: [{ $type: "$spMetadata" }, "missing"] },
then: false,
else: {
$cond: {
if: { $regexMatch: { input: "$spMetadata", regex: /AuthnRequestsSigned="true"/ } },
then: true,
else: false
}
}
}
}
}
}
]
);


db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
propagateLogout: { $exists: false }
},
{
$set: { propagateLogout: false }
}
);

print("END 61_update_existing_external_providers_models.js.j2");
2 changes: 1 addition & 1 deletion docs/developeurs/vitamui-conf-dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ of our environment trusted by the remote environment, to do that, we follow thes
```

3. Download an external public certificate (external_pub.pem && external_key.pem) provided in this
path: https://webdav.dev.programmevitam.fr/webdav/Certificats_vitam/
path: https://rec.part.programmevitam.fr/
4. Generate a keystore, for Vitam context, with this certificate. (example password: customer-password-ks , example in P12 format):

```shell script
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('IdentityProviderService', () => {
let httpTestingController: HttpTestingController;
let identityProviderService: IdentityProviderService;
let identityProviders: any[];
let externalIdentityProviders: any[];
let keystore: File;
let idpMetadata: File;

Expand All @@ -68,6 +69,19 @@ describe('IdentityProviderService', () => {
idpMetadata,
},
];
externalIdentityProviders = [
{
id: '44',
customerId: '1234',
name: 'Test IDP',
internal: false,
keystorePassword: 'testpassword1234',
patterns: ['test.com', 'test.fr'],
enabled: true,
keystore,
idpMetadata,
},
];
const snackBarSpy = jasmine.createSpyObj(['open']);

TestBed.configureTestingModule({
Expand Down Expand Up @@ -108,6 +122,23 @@ describe('IdentityProviderService', () => {
req.flush(identityProviders[0]);
});

it('should call /fake-api/providers and display a succes message to asking to restart service', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.create(externalIdentityProviders[0]).subscribe((response: IdentityProvider) => {
expect(response).toEqual(externalIdentityProviders[0]);
expect(snackBar.open).toHaveBeenCalledTimes(1);
expect(snackBar.open).toHaveBeenCalledWith({
message: 'SHARED.SNACKBAR.PROVIDER_CREATE_RESTART_NEED',
translateParams: {
param1: externalIdentityProviders[0].name,
},
});
}, fail);
const req = httpTestingController.expectOne('/fake-api/providers');
expect(req.request.method).toEqual('POST');
req.flush(externalIdentityProviders[0]);
});

it('should display an error message', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.create(identityProviders[0]).subscribe(fail, () => {
Expand Down Expand Up @@ -153,6 +184,27 @@ describe('IdentityProviderService', () => {
req.flush(identityProviders[0]);
});

it('should call PATCH with specific message /fake-api/providers/44', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.updated.subscribe(
(provider: IdentityProvider) => expect(provider).toEqual(externalIdentityProviders[0]),
fail,
);
identityProviderService.patch(externalIdentityProviders[0]).subscribe((provider: IdentityProvider) => {
expect(provider).toEqual(externalIdentityProviders[0]);
expect(snackBar.open).toHaveBeenCalledTimes(1);
expect(snackBar.open).toHaveBeenCalledWith({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED',
translateParams: {
param1: externalIdentityProviders[0].name,
},
});
}, fail);
const req = httpTestingController.expectOne('/fake-api/providers/44');
expect(req.request.method).toEqual('PATCH');
expect(req.request.body).toEqual(externalIdentityProviders[0]);
req.flush(externalIdentityProviders[0]);
});
it('should display an error message', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.patch(identityProviders[0]).subscribe(fail, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ export class IdentityProviderService {
map((updatedIdp: IdentityProvider) => this.addSpMetadataUrl(updatedIdp)),
tap(
(newIDP: IdentityProvider) => {
let messageKey = newIDP.internal ? 'SHARED.SNACKBAR.PROVIDER_CREATE' : 'SHARED.SNACKBAR.PROVIDER_CREATE_RESTART_NEED';

this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_CREATE',
message: messageKey,
translateParams: {
param1: newIDP.name,
},
Expand All @@ -81,8 +83,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -102,8 +105,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -122,8 +126,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -135,27 +140,6 @@ export class IdentityProviderService {
),
);
}

updateSpMetadataFile(id: string, spMetadata: File): Observable<IdentityProvider> {
return this.providerApi.patchProviderSpMetadata(id, spMetadata).pipe(
map((updatedIdp: IdentityProvider) => this.addSpMetadataUrl(updatedIdp)),
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
translateParams: {
param1: updatedIdp.name,
},
});
},
(error) => {
this.snackBarService.open({ message: error.error.message, translate: false });
},
),
);
}

getAll(customerId?: string): Observable<IdentityProvider[]> {
const criterionArray: Criterion[] = [];
if (customerId) {
Expand Down
2 changes: 2 additions & 0 deletions ui/ui-frontend/projects/identity/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
"CUSTOMER_CREATE_ERROR": "The organization could not be created, please contact an administrator",
"CUSTOMER_UPDATE": "The organization {{ param1 }} has been successfully modified",
"PROVIDER_CREATE": "IDP {{ param1 }} has been successfully created",
"PROVIDER_CREATE_RESTART_NEED": "IDP {{ param1 }} has been successfully created, taking these parameter changes into account requires a restart of the authentication system",
"PROVIDER_UPDATE": "IDP {{ param1 }} has been successfully modified",
"PROVIDER_UPDATE_RESTART_NEED": "IDP {{ param1 }} has been successfully modified,, taking these parameter changes into account requires a restart of the authentication system",
"OWNER_CREATE": "The owner {{ param1 }} has been successfully created",
"OWNER_UPDATE": "The owner {{ param1 }} has been successfully modified",
"SAFE_CREATE": "The safe {{ param1 }} of owner {{ param2 }} has been successfully created",
Expand Down
2 changes: 2 additions & 0 deletions ui/ui-frontend/projects/identity/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
"CUSTOMER_CREATE_ERROR": "L'organisation n'a pas pu être créé, veuillez contacter un administrateur",
"CUSTOMER_UPDATE": "L'organisation {{ param1 }} a bien été modifié",
"PROVIDER_CREATE": "L'IDP {{ param1 }} a bien été créé",
"PROVIDER_CREATE_RESTART_NEED": "L'IDP {{ param1 }} a bien été créé, la prise en compte du changement de ces paramètres nécessite un redémarrage du système d'authentification",
"PROVIDER_UPDATE": "L'IDP {{ param1 }} a bien été modifié",
"PROVIDER_UPDATE_RESTART_NEED": "L'IDP {{ param1 }} a bien été modifié, la prise en compte du changement de ces paramètres nécessite un redémarrage du système d'authentification",
"OWNER_CREATE": "Le propriétaire {{ param1 }} a bien été créé",
"OWNER_UPDATE": "Le propriétaire {{ param1 }} a bien été modifié",
"SAFE_CREATE": "Le coffre {{ param1 }} du propriétaire {{ param2 }} a bien été créé",
Expand Down

0 comments on commit 45954b5

Please sign in to comment.