angularangular-ssr

Using Angular 16 SSR to Secure Sensitive Data


I'm currently working on a recipe viewer website using Angular, and I'm looking to secure some sensitive data from being accessible in the client's browser. The sensitive data includes service modules responsible for making HTTP requests to 3rd-party websites, JSON mappers, model classes, and environment variables including API keys.

I understand that Angular SSR is mainly used for SEO purposes and performance increase. But, I also know that any logic written on the server side will be hidden from the client's browser. Given that my Angular app doesn't use a traditional backend and thus contains business logic within itself, I'm considering using Angular SSR as a means to secure this sensitive data.

Here's an example of a service module that I'd like to secure:


getRecipes(): Observable<Recipe[]> {

  const params = new HttpParams()

    .set('apiKey', this.apiKey)

    .set('number', '16');

  return this.http.get<any>(this.apiUrl + '/complexSearch', { params })

    .pipe(

      map(response => response.results.map(fromApiResponseForList)),

      catchError(error => {

        return of([]);

      })

    );

}

I'm wondering if anyone has experience with using Angular SSR in this way, and if there are any potential pitfalls or considerations I should be aware of. Any advice or guidance would be greatly appreciated.

Thank you in advance for your help!


Solution

  • When dealing with sensitive data that should not be visible to users, it is essential to keep it separate from your client-side code. While it's technically possible to manipulate the HTML code sent to the user, doing so can be complex and error-prone. In this case, you need to write client-side code and server-side code, which need a lot of knowledge.

    However, the easier way is extending your server.ts file, which handles SSR, with a new endpoint. Thereafter, you can call this endpoint directly than the other API. Below is an example with Angular 17.

    server.ts

    import { APP_BASE_HREF } from '@angular/common';
    import { CommonEngine } from '@angular/ssr';
    import express from 'express';
    import { fileURLToPath } from 'node:url';
    import { dirname, join, resolve } from 'node:path';
    import bootstrap from './src/main.server';
    import cors from 'cors';
    import axios from 'axios';
    
    export function app(): express.Express {
      const server = express();
      const serverDistFolder = dirname(fileURLToPath(import.meta.url));
      const browserDistFolder = resolve(serverDistFolder, '../browser');
      const indexHtml = join(serverDistFolder, 'index.server.html');
    
      const commonEngine = new CommonEngine();
    
      // CORS configuration for specific origin
      server.use(cors({ origin: 'http://localhost:4200' }));
    
      server.set('view engine', 'html');
      server.set('views', browserDistFolder);
    
      /// YOUR ENDPOINT
      server.get('/api/recipes', (req, res) => {
        // Sample data
        const data = [
          {
            id: 'pasta',
            name: "Pasta",
            message: "Creamy and rich Alfredo sauce."
          },
          {
            id: 'curry',
            name: "Curry",
            message: "Spicy, with tender chicken."
          },
          {
            id: 'salad',
            name: "Salad",
            message: "Fresh greens and vinaigrette."
          }
        ];
    
        // Send the JSON response
        res.json(data);
    
        // TODO: Use your own logic like below
        /* const apiKey = 'YOUR_API_KEY'; // TODO: Replace with your API key
        const apiUrl = 'YOUR_API_URL'; // TODO: Replace with your API URL
    
        const params = {
          apiKey: apiKey,
          number: '16',
        };
    
        return axios
          .get(apiUrl + '/complexSearch', { params })
          .then((response) => {
            return response.data.results.map(fromApiResponseForList);
          })
          .catch((error) => {
            console.error('Error:', error);
            return [];
          }); */
      });
    
      server.get('*.*', express.static(browserDistFolder, {
        maxAge: '1y'
      }));
    
      // All regular routes use the Angular engine
      server.get('*', (req, res, next) => {
        const { protocol, originalUrl, baseUrl, headers } = req;
    
        commonEngine
          .render({
            bootstrap,
            documentFilePath: indexHtml,
            url: `${protocol}://${headers.host}${originalUrl}`,
            publicPath: browserDistFolder,
            providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
          })
          .then((html) => res.send(html))
          .catch((err) => next(err));
      });
    
      return server;
    }
    
    function run(): void {
      const port = process.env['PORT'] || 4000;
    
      // Start up the Node server
      const server = app();
      server.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }
    
    run();
    
    

    app.component.ts

    import { Component, inject } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterOutlet } from '@angular/router';
    import { HttpClient } from '@angular/common/http';
    import { Observable, catchError, of } from 'rxjs';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [
        CommonModule,
        RouterOutlet,
      ],
      templateUrl: './app.component.html',
      styleUrl: './app.component.scss'
    })
    export class AppComponent {
      private http = inject(HttpClient);
      recipes$!: Observable<Recipe[]>;
    
      ngOnInit(): void {
        this.getRecipes();
      }
    
      getRecipes() {
        // TODO: Optionally use environment to use a production and development url
        const apiUrl = 'http://localhost:4000/api/recipes'; 
    
        this.recipes$ = this.http.get<Recipe[]>(apiUrl).pipe(
          catchError(error => {
            console.error(error);
            return of([]);
          })
        );
      }
    }
    
    interface Recipe {
      id: string,
      name: string,
      message: string,
    }
    

    app.component.html

    @if(recipes$|async; as recipes){
    <ul>
      @for(recipe of recipes; track recipe.id){
      <li>{{ recipe.name }} - {{ recipe.message }}</li>
      }
    </ul>
    }
    

    If you are familiar with Google Cloud Functions, also known as Firebase Functions, it is also a good way to use it. You can easily enhance the security of your application by integrating it with Secret Manager, a reliable tool for storing API keys and other sensitive information safely. This approach ensures that your keys are not exposed to users and provides an additional layer of protection for your application.