diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5985ad58215c193ec08fc3bf83112b019d07c5ae..b5f8e2e11a73684091a1baaf0af329ce3aa72f95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.14.1] - 2025-03-27
+
+[0.14.1]: https://git.opentalk.dev/opentalk/backend/services/recorder/-/compare/v0.14.0...v0.14.1
+
+### 🐛 Bug fixes
+
+- Recorder timeout for first recording attempt ([!448](https://git.opentalk.dev/opentalk/backend/services/recorder/-/merge_requests/448), [#204](https://git.opentalk.dev/opentalk/backend/services/recorder/-/issues/204))
+
+### 📚 Documentation
+
+- Added advisory for unmaintained paste crate ([!446](https://git.opentalk.dev/opentalk/backend/services/recorder/-/merge_requests/446))
+
+### 📦 Dependencies
+
+- (deps) Update rust crate ring to v0.17.14 ([!452](https://git.opentalk.dev/opentalk/backend/services/recorder/-/merge_requests/452))
+
 ## [0.14.0] - 2025-03-05
 
 [0.14.0]: https://git.opentalk.dev/opentalk/backend/services/recorder/-/compare/v0.13.2...v0.14.0
diff --git a/Cargo.lock b/Cargo.lock
index a0a020c6e3909ba5e2ba56b3e9421f1ff1a35197..067a729a4ad55a8413d1ca993afd2c9dadfcce6a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3180,9 +3180,11 @@ checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
 dependencies = [
  "base64 0.22.1",
  "js-sys",
+ "pem",
  "ring",
  "serde",
  "serde_json",
+ "simple_asn1",
 ]
 
 [[package]]
@@ -3723,16 +3725,18 @@ dependencies = [
 
 [[package]]
 name = "opentalk-recorder"
-version = "0.14.0"
+version = "0.14.1"
 dependencies = [
  "anyhow",
  "bytes",
+ "chrono",
  "clap",
  "cocoa",
  "config",
  "env_logger 0.11.6",
  "futures",
  "gstreamer",
+ "jsonwebtoken",
  "lapin",
  "log",
  "objc",
@@ -4073,6 +4077,16 @@ dependencies = [
  "hmac",
 ]
 
+[[package]]
+name = "pem"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
 [[package]]
 name = "pem-rfc7468"
 version = "0.7.0"
@@ -4734,9 +4748,9 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.17.11"
+version = "0.17.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
 dependencies = [
  "cc",
  "cfg-if",
@@ -5292,6 +5306,18 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 
+[[package]]
+name = "simple_asn1"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror 2.0.12",
+ "time",
+]
+
 [[package]]
 name = "siphasher"
 version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 283ec6afe27ac3a8177211bbffd4842adb187b76..00e7f05156cbe28e5e3f7e7be60f27e78f090e24 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,17 +10,19 @@ license = "EUPL-1.2"
 name = "opentalk-recorder"
 publish = false
 repository = "https://gitlab.opencode.de/opentalk/recorder"
-version = "0.14.0"
+version = "0.14.1"
 build = "build.rs"
 
 [dependencies]
 anyhow = { version = "1", features = ["backtrace"] }
 bytes = "1"
+chrono = "0.4"
 compositor = { package = "opentalk-compositor", version = "0.14.0" }
 config = { version = "0.15", default-features = false, features = ["toml"] }
 env_logger = "0.11"
 futures = "0.3"
 gst = { package = "gstreamer", version = "0.23" }
+jsonwebtoken = "9.3"
 lapin = { version = "2.3", default-features = false, features = [
   "rustls-webpki-roots-certs",
 ] }
diff --git a/deny.toml b/deny.toml
index a93c85c3c4a5ff435cf6fa1fb07b59b17a91eddd..8e9aa0bd7e3dbd5e9c8329f0c7069001750e275b 100644
--- a/deny.toml
+++ b/deny.toml
@@ -40,7 +40,13 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
 yanked = "deny"
 # A list of advisory IDs to ignore. Note that ignored advisories will still
 # output a note when they are encountered.
-ignore = ["RUSTSEC-2023-0071"]
+ignore = [
+  "RUSTSEC-2023-0071",
+  # This is an advisory for the 'paste' crate, which is no longer maintained
+  # Since this is a dependency for gstreamer, we can't really do anything
+  # against it, but wait for a new gstreamer release.
+  "RUSTSEC-2024-0436"
+]
 # Threshold for security vulnerabilities, any vulnerability with a CVSS score
 # lower than the range specified will be ignored. Note that ignored advisories
 # will still output a note when they are encountered.
diff --git a/src/http.rs b/src/http.rs
index 749931d6c58352a6131bfbbb895cf0295d0e1b1a..5615034aec680c2c106da56825db714cf73de90b 100644
--- a/src/http.rs
+++ b/src/http.rs
@@ -4,10 +4,12 @@
 
 //! HTTP calls made by this library (except for websockets)
 
-use std::time::{Duration, Instant};
+use std::time::{Duration, Instant, SystemTime};
 
 use anyhow::{bail, Context, Result};
+use chrono::{serde::ts_seconds::deserialize as from_ts, DateTime, Utc};
 use futures::{SinkExt, StreamExt};
+use jsonwebtoken::{self, decode, DecodingKey, Validation};
 use openidconnect::{
     core::{
         CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreErrorResponseType, CoreGenderClaim,
@@ -139,24 +141,6 @@ impl HttpClient {
         })
     }
 
-    async fn refresh_access_tokens(&self, invalid_token: AccessToken) -> Result<()> {
-        let mut token = self.access_token.write().await;
-
-        if token.secret() != invalid_token.secret() {
-            return Ok(());
-        }
-
-        let response = self
-            .oidc
-            .exchange_client_credentials()
-            .request_async(&self.client)
-            .await?;
-
-        *token = response.access_token().clone();
-
-        Ok(())
-    }
-
     pub(crate) async fn start(
         &self,
         settings: &ControllerSettings,
@@ -165,45 +149,32 @@ impl HttpClient {
     ) -> Result<String> {
         let uri = format!("{}/services/recording/start", settings.v1_api_base_url());
 
-        // max 10 authentication tries
-        for _ in 0..10 {
-            let token = {
-                // Scope the access to the lock to avoid holding it for the entire loop-body
-                let l = self.access_token.read().await;
-                l.clone()
-            };
+        let token = self.get_valid_access_token().await?;
 
-            let response = self
-                .client
-                .post(&uri)
-                .bearer_auth(token.secret())
-                .json(&StartRequest {
-                    room_id,
-                    breakout_room,
-                })
-                .send()
-                .await?;
+        let response = self
+            .client
+            .post(&uri)
+            .bearer_auth(token.secret())
+            .json(&StartRequest {
+                room_id,
+                breakout_room,
+            })
+            .send()
+            .await?;
 
-            match response.status() {
-                StatusCode::OK => {
-                    let response = response.json::<StartResponse>().await?;
+        match response.status() {
+            StatusCode::OK => {
+                let response = response.json::<StartResponse>().await?;
 
-                    return Ok(response.ticket);
-                }
-                StatusCode::UNAUTHORIZED => {
-                    let ApiError { code } = response.json::<ApiError>().await?;
+                Ok(response.ticket)
+            }
+            StatusCode::UNAUTHORIZED => {
+                let ApiError { code } = response.json::<ApiError>().await?;
 
-                    if code == "unauthorized" {
-                        self.refresh_access_tokens(token).await?;
-                    } else {
-                        bail!(InvalidCredentials);
-                    }
-                }
-                code => bail!("unexpected status code {code:?}"),
+                bail!("failed to authorize, {code:?}");
             }
+            code => bail!("unexpected status code {code:?}"),
         }
-
-        bail!("failed to authorize")
     }
 
     pub(crate) async fn upload_render(
@@ -226,19 +197,10 @@ impl HttpClient {
             urlencoding::encode(&timestamp.to_string()),
         );
 
+        let token = self.get_valid_access_token().await?;
+
         log::debug!("connect websocket to {uri}");
-        let ws_stream = if let Ok((ws_stream, _response)) =
-            self.websocket_connect(uri.clone()).await
-        {
-            ws_stream
-        } else {
-            log::debug!("Unable to connect to the websocket, refresh access token and retry it");
-            self.refresh_access_tokens(self.access_token.read().await.clone())
-                .await
-                .context("unable to refresh the access token")?;
-
-            self.websocket_connect(uri).await?.0
-        };
+        let (ws_stream, _response) = self.websocket_connect(uri.clone(), token).await?;
         let (mut tx, mut rx) = ws_stream.split();
 
         let mut last_pong = Instant::now();
@@ -293,18 +255,31 @@ impl HttpClient {
         Ok(())
     }
 
+    async fn get_valid_access_token(&self) -> Result<AccessToken> {
+        let mut token = self.access_token.write().await;
+
+        // Refresh access token if it's expired
+        if check_if_token_is_expired(token.secret())? {
+            let response = self
+                .oidc
+                .exchange_client_credentials()
+                .request_async(&self.client)
+                .await?;
+
+            *token = response.access_token().clone();
+        }
+
+        Ok(token.clone())
+    }
+
     async fn websocket_connect(
         &self,
         uri: String,
+        token: AccessToken,
     ) -> Result<(
         WebSocketStream<tt::MaybeTlsStream<tokio::net::TcpStream>>,
         openidconnect::http::Response<std::option::Option<Vec<u8>>>,
     )> {
-        let token = {
-            let l = self.access_token.read().await;
-            l.clone()
-        };
-
         let mut request = uri.into_client_request().unwrap();
         request.headers_mut().insert(
             reqwest::header::AUTHORIZATION,
@@ -316,10 +291,25 @@ impl HttpClient {
     }
 }
 
-/// Error returned by the `start` function when the given digits were incorrect
-#[derive(Debug, thiserror::Error)]
-#[error("given credentials were invalid")]
-pub(crate) struct InvalidCredentials;
+#[derive(Deserialize)]
+struct TokenClaims {
+    #[serde(deserialize_with = "from_ts")]
+    exp: DateTime<Utc>,
+}
+
+/// Check if the token is expired or is expiring within a minute
+fn check_if_token_is_expired(token: &str) -> Result<bool> {
+    let mut validation = Validation::default();
+    validation.insecure_disable_signature_validation();
+    validation.validate_exp = false;
+    validation.validate_aud = false;
+
+    let token = decode::<TokenClaims>(token, &DecodingKey::from_secret(&[]), &validation)?;
+
+    let now = DateTime::<Utc>::from(SystemTime::now());
+
+    Ok(now > token.claims.exp + Duration::from_secs(60))
+}
 
 #[derive(Serialize)]
 struct StartRequest<'s> {
diff --git a/src/main.rs b/src/main.rs
index e57dcbdc823ad7e1b0073438a0dee71f666f5c31..8fd6b09f13d2114280b2a66b4fb109a5a9cb279a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -53,7 +53,6 @@ fn check_plugins() -> Result<()> {
         "compositor",
         "debug",
         "dtls",
-        "fdkaac",
         "pango",
         "png",
         "rtp",