1

I have a confluence of a few things that are all great individually but when I bring them together, they are causing problems.

I have an Angular SPA, updated to Angular 20, that uses a Keycloak server for OAuth2 authentication and, when deployed, runs in what is essentially a Docker container.

Angular SPAs don't always play well with Docker, which I resolved by using hash routing as outlined here. Everything was going well until I tried to log in. It turns out that hash routing really confuses my OAuth server. The /auth endpoint in the server tries to redirect here after login

http://localhost:4201/?state=c28xOTNSNC5HcHl0UkpWQXV6NnRwSm5MSjhjMWFrdDlrRWRrOUxmSm5BbmpZ&session_state=0ad042e8-2404-4f99-aa05-bcb4515287c8&iss=http%3A%2F%2Fkeycloak.psn.local%3A5005%2Fauth%2Frealms%2Fsocial-network-ecosystem&code=1a269f9e-09e7-410d-9385-b6c9a5405d44.0ad042e8-2404-4f99-aa05-bcb4515287c8.fe697efd-d696-444a-894a-6e6720dde121#/home

Which you will note is the host url, all the query params, and then the route info. Angular doesn't know what to do with that, so the UI doesn't track the tokens. Keycloak does not consider this a bug on their end. The GitHub issues for angular2-oauth-oidc, the login lib I've been using, suggests using the query response mode, but I'm getting the same issue as the person in the final comment, a failure on post /

My final plan was to move to SSR, which I'm told can fix routing in Docker without the hashes, but this lib angular2-oauth-oidc doesn't work well with it. They link to this guide in their documentation, but I didn't get any motion trying to implement it, possibly because it's from 2017.

So I have a couple possible solutions, any one of which should solve my problem. I need a way to do routing in Docker without hashes or SSR. I need to get SSR to work with my oauth lib. I need a way to get Keycloak to handle a hashed redirect URI in a way Angular can accept.

The last solution I have is to migrate to SSR and make my own login handling.

5
  • I can't help with Angular, but I might comment in case it helps with the general direction. A FE framework should not care that it is running in Docker, and indeed it probably isn't aware that it is running in Docker. The linked question makes me wonder if the FE and BE are running on different ports on a dev machine, and thus the problem may have been CORS, not Docker. Commented Sep 29 at 21:01
  • It's because the Docker container doesn't run Angular itself, it runs a server like Node or nginx that hosts the Angular app. Those servers expect to serve static content so if they get a link to "/home" they will look for an actual file or folder that, obviously, doesn't exist. If you go to directly to your index.html it'll load fine and the routing will work from there, but you can't link directly to anything other than that. Commented Sep 30 at 1:10
  • Sure, to server Angular in Docker, there needs to be a web server in the container. This could be at something like http://localhost:8080. But if you were not using Docker, you'd still need the web server on your dev machine, and you'd still need to fetch from something like http://localhost:8080. Commented Oct 1 at 19:32
  • Perhaps the issue is that without Docker, you have a folder from where /home can be retrieved. Isn't the solution then to do the same inside Docker, maybe using a bind mount? In other words, I am not sure you've shown that Docker changes anything, and it looks like the answer below explains that Docker works for the author. Commented Oct 1 at 19:34
  • Kinda. What the person in the answer below describes is a configuration for your server that redirects any 404 to the index page, which is the SPA, and because your request is still to /home the router is then able to find it. There's also a guide for this in the first link I put in the original question. So it's possible to route through that server in a way that is, at least to the end user, the same as routing in ng serve. The problem here turned out to be that I had set up that fallback to index incorrectly, and I failed to identify that as the problem until I asked here. Commented Oct 1 at 21:24

1 Answer 1

2

Usually for an SPA you use path based routing and the web host needs code to serve the default document regardless of the physical path.

DEFAULT DOCUMENT SERVING

As an example, this is for a Node.js Express web host, where the get statement handles not found paths like /callback by serving the index.html file:

const spaBasePath = './';
const spaPhysicalRoot = './';
express.use(spaBasePath, express.static(spaPhysicalRoot));

express.get('*_', (request, response) => {
    response.sendFile(
        'index.html',
        {root: spaPhysicalRoot})
};

DOCKER HOSTING FOR WEB STATIC CONTENT

It is common to use an API gateway like NGINX as the entry point to APIs. If you use container-based hosting for web static content, you can add an extra hostname to the gateway for the web origin. A deployment expressed in Docker might look like this and would work for an Angular client:

services:
  apigateway:
    image: nginx:latest
    hostname: apigateway-internal
    ports:
    - 443:3000
    volumes:
    - ./default.conf:/etc/nginx/templates/default.conf.template
    - ./example.ssl.key:/usr/local/share/certs/example.ssl.key
     - /example.ssl.crt:/usr/local/share/certs/example.ssl.crt

  webhost:
    image: mywebhost:latest
    hostname: mywebhost-internal
    environment:
      PORT: '3001'

The routes in the default.conf.template file might look like this, to forward requests from the gateway to the web host. The resolver makes the hostnames work on a development computer.

resolver 127.0.0.11;

server {
    server_name www.myapp.example;
    listen      3001 http2 ssl;
    ssl_certificate     /usr/local/share/certs/example.ssl.crt;
    ssl_certificate_key /usr/local/share/certs/example.ssl.key;

    location / {
        proxy_pass http://mywebhost-internal:3001;
    }
}

Although it is useful to put together Docker deployments during development, production deployments tend to work differently. I describe a couple of common options next.

CLOUD NATIVE HOSTING

In real world cloud native deployments of static content you use platforms like Kubernetes and provide an ingress route. Certificates and hostnames are expressed in a parent resource. Typically the gateway loads certificates issued (and automatically renewed) by a component like cert-manager.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: webhost-route
spec:
  parentRefs:
  - name: kong-gateway
    namespace: apigateway
  hostnames:
  - www.myapp.example
  rules:
  - matches:
    - path:
        value: /
    backendRefs:
    - name: webhost-svc
      kind: Service
      port: 3001

CONTENT DELIVERY NETWORKS

Personally, I prefer to use a content delivery network for the web side of the architecture since it has global performance benefits. For example, in AWS Cloudfront you can use a viewer request function like this to serve the default document for unknown paths:

function handler(event) {
    
    const request = event.request;
    const requestUri = request.uri.toLowerCase();

    const extensions = [
        '.html',
        '.js',
        '.css'
    ];

    const knownExtension = extensions.find((ext) => {
        return requestUri.endsWith(ext);
    });

    if (!knownExtension) {
        request.uri = `/index.html`;
    }
    
    return request;
}

PORTABILITY

As you can see, Docker and Angular can play nicely, though I also found techniques to serve web static content for a web application tricky when I first ran into them. They are not always explained well.

Regardless of technology preferences (Angular, React etc) or hosting preferences (cloud native, CDN), the techniques I describe are portable. You should be able to follow them and keep your future technical options open.

Sign up to request clarification or add additional context in comments.

4 Comments

I have no attachment to SSR. I wasn't using it before I started this process. But from what I've been reading it seems like one of the possible solutions. I've also been reading that it is generally considered superior for just about every use case except the one I'm struggling with right now. I had already implemented some fallback logic, I think the same as yours but in NGINX, but based on your comment I have been going back to it and it looks like this all could just be a mistake there. Still testing, but so far it seems to be working with path routing without SSR.
Cool - get a curl request working against a non existent physical path and hopefully it will simplify your OAuth behaviors. Providers work less well with hash based routing these days.
Gary, a suggestion: the OP says in their question that "Angular SPAs don't always play well with Docker", but IMO they've failed to substantiate that in the comments. I can't work out what they mean, but perhaps you can. Would you be able to edit the question with future readers in mind, so that the essence of the question remains, minus any misleading canards?
Good idea - I made some updates about serving of static content. It usually works differently in developer setups to deployed systems. If developers start with a local Docker deployment they need to progress it to production and the use of production web hosts, so I summarize a couple of mainstream options. Like many questions that I answer, there is more involved than just Angular and Docker, and the OP can benefit from my experience in this area. I return to the OP point about "Angular SPAs don't always play well with Docker" in the summary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.