In the second part of our tutorial, learn how to set up authentication for an Angular app that consumes Sitefinity CMS OData services using Sitefinity’s easy-to-use OData SDK.
You are probably already acquainted with our previous blog post about building an Angular app with the help of Sitefinity CMS headless capabilities. If so, you know by now how to use the Sitefinity OData SDK for read operations, and for consuming data from Sitefinity and displaying it in an Angular app.
In this blog post, we will demonstrate how to create authentication for your Angular app, utilizing Sitefinity authentication features. You will also learn how to perform write operations via the SDK by creating comments, testimonials, and by uploading images. You will use the QuantumHeadlessAngularApp you previously created. You can also use a ready version of this app, located in the GitHub repo.
Following is an overview of what you need to do to create authentication for your app.
Once you create a Sitefinity website, you can access all content types via the Default OData service, located under Administration -> WebServices. To access the API reference page for the service, click the Use in your app link on the right. Of course, depending on your scenario, you can use your own custom service, different than the default one.
By default, Sitefinity OData web services are secured and their access permissions are set to Administrators only. The other available permission options are Everyone and Authenticated. As you would expect, the Authenticated option provides read and write permissions only to authenticated users. On the other hand, the Everyone option provides read permissions to everyone and write permissions only to authenticated users.
Since your application needs to be visible to everyone, but comments and testimonials to be created only by Sitefinity users, you need to switch to the Everyone option for your OData service.
By default, Sitefinity CMS uses claims-based identification. In this scenario, the identity of users is authenticated by Security Token Service (STS). The STS issues a token, containing the claims that the user makes about their identity. When requesting a protected resource from Sitefinity via HTTP, you need to obtain an authentication token and pass it as an Authorization header. Since you are working with the OData SDK, you need to pass the authentication token to a designated authentication property of the SDK Sitefinity instance.
By following the documentation guidelines, you use the oidc-client library to manage the overall security context of the application. This includes signing in, signing out, issuing an authentication token from the STS, and providing identity information about the current user. Usually applications have more than one provider but in this example, you will only be working with the Sitefinity STS as an Identity provider.
Let’s get going!
Add the oidc-client as an npm dependency and install with:
npm install oidc-client --save
Authentication for your app basically consists of:
There are two ways to sign in users - via a redirect to the Identity server’s login page or silently.
private authenticateWithRedirects(returnUrl: string): Observable<void> {
const signIn = this.manager.signinRedirect({ data: returnUrl });
return observableFrom(signIn);
}
After logging in, the user is redirected to the URL that you set in the redirect_uri property of the UserManager settings.
However, you want to be able to return to the application page from which login or logout was initiated. To do this, you create two components – sign-in-redirect.component and sign-out-redirect component. A few steps back, you actually pointed the redirect_uri and the post_logout_redirect_uri properties of the settings object to the routes of these components. Thus, after login or logout, users are redirected to the corresponding component. The code of the two components is quite similar. The login and logout methods of the UserManager class accept the URL of the page you want your users to return to after the action is performed.
After login, users are redirected to the route of the sign-in-redirect.component. In the component, on ngOnInit, you subscribe to the signingRedirectCallback. The observable returns the user, as well as the URL passed to the signinRedirect method as a state property of the user. Finally, navigate back to the route where the user initially was. The logic on logout is similar to that of login.
ngOnInit() {
const redirect = new UserManager({}).signinRedirectCallback();
const redirectAsObservable = observableFrom(redirect);
redirectAsObservable.subscribe((args: SignInResponse) => {
const state = args.state;
this.router.navigateByUrl(state);
});
}
In case the user session expires while browsing the app, perform an attempt for silent authentication:
private authenticateSilent(returnUrl: string): Observable<void> {
const signInSilent = observableFrom(this.manager.signinSilent());
signInSilent.subscribe(() => {
this.router.navigateByUrl(returnUrl);
});
return signInSilent.pipe(map(x => <any>x));
}
On successful silent login, redirect back to the page from which the silent login was initiated.
Once the user is logged in, you get the authentication token from the user object and emit it though the token property of the provider.
The provider also exposes a signout method, a getUser method, and an isLoggedIn method. The first two call methods of the UserManager class. The isLoggedIn method gets the user and the session and returns a boolean value. It returns true if the user session hasn't expired and the subject identifier of the user and the session are the same.
isLoggedIn(): Observable<boolean> {
const user = observableFrom(this.manager.getUser());
const session = observableFrom(this.manager.querySessionStatus());
return observableCombineLatest(user, session).pipe(map(data => {
const [user, session] = data;
if (user && session) {
if (!user.expired && user.profile.sub === session.sub) {
return true;
}
}
return false;
}), catchError(() => {
return observableOf(false);
}));
}
The authentication service initializes the provider and sets the authentication token to the Sitefinity object of the SDK. It also exposes generic authentication methods. Currently there is just one provider, but the authentication service makes it easy to plug in more. The only condition for the providers is to implement a common interface—AuthProviders.
As mentioned earlier, the idea is to allow unauthenticated users to see the content of your application, whereas allow only authenticated to create content—post comments and create testimonials. This is why you need an authentication guard for the protected routes.
In the CanActivate method of the authentication guard you check whether the user is logged in and if not, redirect them to the Sitefinity login page. You also pass the current route to the signIn method to make sure that the user is redirected back to the same page.
canActivate (next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
const isLoggedInSubject = new ReplaySubject<boolean>(1);
this.authService.init().subscribe(() => {
this.authService.isLoggedIn().subscribe((isLoggedIn) => {
if(!isLoggedIn) {
this.authService.signIn(state.url).subscribe();
}
isLoggedInSubject.next(isLoggedIn);
isLoggedInSubject.complete();
})
});
return isLoggedInSubject.asObservable();
}
In your application, this guard is used to protect the ‘testimonial’ create route.
The News details component has a Comments section. We will get back to the implementation of this section a later in the tutorial. What is important for the authentication at this point is that if a user is not logged in, they do not see the Submit a comment form. They only see the already submitted comments and a link to the login page. To achieve this, you use the authentication service to check if the user is logged in.
By now, the authentication for your app is all set up. Let’s move on to the write operations.
To add a Comments form to the News details component, you create a comments component, which consists of the list of already submitted comments and a comment submit form. The comment component has two main methods:
submitComment(form: any) {
if(form.valid) {
this.creatingComment = true;
this.commentsService.createComment(newsItemsDataOptions, this.model, this.commentableItemId).subscribe(isCommentCreated => {
this.isCommentCreated = isCommentCreated;
this.getComments();
});
}
}
SubmitComment calls the comments service and in case the comment is successfully submitted, updates the list of comments with the new comment.
The getComments method of the comments service returns an Observable of comments. Comments are exposed as a complex property to the content types that support comments. Therefore, the request through the SDK is a little different than the requests made so far:
getComments(contentItemsDataOptions: DataOptions, contentItemId: string, skip?: number, take?: number ): Observable<Comment[]> {
const commentSubject = new ReplaySubject<Comment[]>(1);
this.sitefinity.instance.data(contentItemsDataOptions).getSingle({
key: contentItemId,
action: 'Comments',
successCb: data => { return commentSubject.next(data.value as Comment[])},
failureCb: data => console.log(data)
});
return commentSubject.asObservable();
}
You call the getSingle method of the Sitefinity instance and pass the Id of the news item as key. In the action property, you specify the name of the complex property—Comments.
The createComment method calls create on the Sitefinity object and passes ‘postcomment’ as an action.
getComments(contentItemsDataOptions: DataOptions, contentItemId: string, skip?: number, take?: number ): Observable<Comment[]> {
const commentSubject = new ReplaySubject<Comment[]>(1);
this.sitefinity.instance.data(contentItemsDataOptions).getSingle({
key: contentItemId,
action: 'Comments',
successCb: data => { return commentSubject.next(data.value as Comment[])},
failureCb: data => console.log(data)
});
return commentSubject.asObservable();
}
Testimonials is a dynamic type in the Quantum project. In your app, testimonials are displayed in a carousel on the Showcases page.
The testimonials component is a carousel component, displaying all testimonials for the site. For the carousel functionality, you are using the ngx-bootstrap npm package that contains all core bootstrap components as Angular components.
The implementation of this component is similar to the implementation of the news list component.You have a Submit testimonial button, leading to the Create testimonial form. As mention earlier, the testimonial route is a protected one and is accessible to users only after authentication.
The testimonial form component is a simple form with required Author, Photo, and Quote fields. The Photo field is an input field of type file. When the user submits a valid form, the createTestimonial method of the testimonial service is called, passing the contents of the form.
The testimonial service has two methods—createTestimonial and getTestimonial. We will focus on the createTestimonial method. This method receives a testimonial object as a parameter. To create a testimonial, you first want to upload the image file. You do this by getting the ID of the library where you want to store the image, in this case you upload it to the Default library. Next, you call the upload method of the ImageService by passing the library ID and the file.
The image is uploaded with a batch operation that passes either a success, a progress, or a reject callback to the batch constructor, as well as a configuration object that contains information about the content provider and the language. Next, you call the upload method of the transaction by passing the file, a safe object URL, the content type, and an additional primitive imageProperties. In the success callback, you create a results object, containing the booleans for the status of the upload, a string property for the error message, and a result that contains the uploaded image.
public uploadImage(libraryId: string, imageProperties: any ): Observable<any> {
const upload = { success: false, failure: false, result: null, errorMessage: null };
const resultSubject = new ReplaySubject<any>(1);
const success = (result) => {
const { data } = result.data[0].response[0];
if (result.isSuccessful) {
upload.result = data;
upload.success = true;
resultSubject.next(upload);
resultSubject.complete();
} else {
upload.failure = true;
upload.errorMessage = data.error;
resultSubject.next(upload);
}
};
const reject = (result) => {
upload.failure = true;
resultSubject.next(upload);
};
const progress = () => {};
const batch = this.sitefinity.instance.batch(success, reject, progress, {
providerName: "OpenAccessDataProvider",
cultureName: "en"
});
const transaction = batch.beginTransaction();
const file = imageProperties.File || imageProperties.file;
const url = window.URL.createObjectURL(file);
const safeUrl = this.sanitizer.bypassSecurityTrustUrl(url);
const imagePrimitives: ImagePrimitives = {
ParentId: libraryId,
DirectUpload: true,
Height: imageProperties.height,
Width: imageProperties.width,
Title: imageProperties.File.name || imageProperties.file
};
const uploadedFile = transaction.upload({
entitySet: "images",
data: file,
dataUrl: safeUrl,
contentType: file.type,
fileName: file.name,
uploadProperties: imagePrimitives
});
transaction.operation({
entitySet: "images",
key: uploadedFile,
data: {
action: "Publish"
}
});
batch.endTransaction(transaction);
batch.execute();
return resultSubject.asObservable();
}
Let’s go back to the createTestimonial method. You subscribe to the uploadImage method of the Image service and once the image is uploaded and received, you create a new batch for the testimonial creation. Next, you call transcation.Create() by passing the type of the content you want to create and the primitive properties of the item.
createTestimonial(testimonial: Testimonial): Observable<boolean> {
const isTestimonialCreated = new ReplaySubject<boolean>(1);
const sortedFields = this.imageService.sortFieldValues(testimonial);
const primitiveFields = sortedFields.primitives;
const relationalFields = sortedFields.relational;
this.imageService.getLibraryByTitle("Default library").subscribe((library: any) => {
const parentId = library.RootId ? library.RootId : library.Id;
this.imageService.uploadImage(parentId, relationalFields["Photo"]).subscribe((upload => {
if (upload.success) {
const success = (result) => {
if (result.isSuccessful) {
isTestimonialCreated.next(true);
} else {
isTestimonialCreated.next(false);
}
};
const failure = () => {
isTestimonialCreated.next(false);
};
const batch = this.sitefinity.instance.batch(success, failure, { providerName: testimonialDataOptions.providerName, cultureName: testimonialDataOptions.cultureName });
const transaction = batch.beginTransaction();
const entitySet = "testimonials";
const operation = { action: "Publish" };
const testimonialItemId = transaction.create({
entitySet,
data: primitiveFields
});
this.imageService.associateRelatedImage("Photo", relationalFields["Photo"], entitySet, upload.result.Id, testimonialItemId, transaction);
transaction.operation({
entitySet: entitySet,
key: testimonialItemId,
data: operation
});
batch.endTransaction(transaction);
batch.execute();
} else {
isTestimonialCreated.next(false);
}
}));
});
return isTestimonialCreated.asObservable();
}
The uploaded image is a complex object, related to the testimonial though content links. To associate the image to the testimonial, you call the associateRelatedImage method of the Image service. You first destroy any existing relations for the related image field. Then, create a new relation. The createRelated method accepts an object with the following properties:
EntitySet
The content type of our item—testimonials
Key
The ID returned by the create transaction
NavigationProperty
The name of the related Image property on the Testimonials type
Link
A URL containing the type of the relatedContent (images) and the ID of the particular relatedItem (the uploaded image)
public associateRelatedImage(relationalFieldName: string, relationalField: {}, entitySet: string, itemId: any, relationId: string, transaction: any) {
transaction.destroyRelated({
entitySet: entitySet,
key: relationId,
navigationProperty: relationalFieldName
});
const relationLink = this.settingsService.url + endpoint + 'images(' + itemId + ')';
transaction.createRelated({
entitySet: entitySet,
key: relationId,
navigationProperty: relationalFieldName,
link: relationLink
});
}
The last thing to do is end the transaction and execute the batch. As a result, if you go back to Showcases, you should see the newly create testimonial item.
We hope you found this tutorial useful. If you have any questions, please drop us a line in the comments below. If you're new to Sitefinity, you can learn more here or jump right in and start your free 30-day trial today.
Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.
Learn MoreSubscribe to get all the news, info and tutorials you need to build better business apps and sites