Hi i have integrated this <https://docs.descope.co...
# ask-a-descoper
a
Hi i have integrated this https://docs.descope.com/getting-started/oidc-endpoints#guide-to-using-oidc-endpoints using this point from the doc Using PKCE in Your Endpoints but in this first it create code and then go to descope flow where we display our bring your own screen rather than direct descope flow UI, but sometime it redirect back to the byos but doesn't create the code upon redirection. It not happens always but sometimes. Can you help us identify what could be the issue this is angular component which redirect's the ui to create code and with OIDC application and redirect back to signup page
Copy code
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { environment } from '../../environments/environment';

@Component({
  selector: 'app-auth-init',
  standalone: true,
  template: `<div style="text-align: center; margin-top: 50px;">Redirecting to secure signup...</div>`
})
export class AuthInitComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  async ngOnInit() {
    const params = this.route.snapshot.queryParamMap;
    const customerId = params.get('customer_id');
    const email = params.get('email');
    const firstName = params.get('first_name');
    const lastName = params.get('last_name');

    if (customerId) sessionStorage.setItem('signup_customer_id', customerId);
    if (email) sessionStorage.setItem('signup_email', email);
    if (firstName && lastName) sessionStorage.setItem('signup_name', `${firstName} ${lastName}`);

    const codeVerifier = this.generateCodeVerifier();
    const codeChallenge = await this.generateCodeChallenge(codeVerifier);
    const state = this.generateState();

    sessionStorage.setItem('pkce_code_verifier', codeVerifier);
    sessionStorage.setItem('pkce_state', state);

    const signupAuthUrl = new URL(environment.descopeSignupAuthUrl);

    const authUrl = new URL(signupAuthUrl);
    authUrl.searchParams.append('client_id', environment.descopeProjectId);
    authUrl.searchParams.append('redirect_uri', `${window.location.origin}/signup`); 
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', 'openid profile email');
    authUrl.searchParams.append('code_challenge', codeChallenge);
    authUrl.searchParams.append('code_challenge_method', 'S256');
    authUrl.searchParams.append('state', state);

    window.location.href = authUrl.toString();
  }

  private generateCodeVerifier(): string {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return this.base64UrlEncode(array);
  }

  private async generateCodeChallenge(verifier: string): Promise<string> {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    return this.base64UrlEncode(new Uint8Array(hash));
  }

  private base64UrlEncode(array: Uint8Array): string {
    return btoa(String.fromCharCode(...array)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }

  private generateState(): string {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return this.base64UrlEncode(array);
  }
}
this is my auth url created: https://api.descope.com/oauth2/v1/authorize?client_id=P2uxgoL1Y0TWOdcHMn97uSLjRFYW&amp;r[…]yIVbQ&amp;code_challenge_method=S256&amp;state=AENZaEbylLoc0rwCl8tM3g
a
Hi @Aksha Malik Thanks for reaching out, Could you please share a HAR/network trace of one failed attempt ?
a
let me check
will this help?
please let me know if i need to provide anything else as well
a
Sure, Thanks!
a
pleae let me know how i can fix this thanks
a
From the HAR and logs, the OIDC transaction is being completed under a different OIDC context than the one it was started with, which is why the authorization code is not always returned. To confirm the correct setup, could you please clarify: 1. Are you using the default Descope OIDC (project-level) or a custom OIDC / SSO application in the console? 2. If you are using an SSO / OIDC application: what value are you using as the
client_id
in the
/authorize
request?
a
1. i am using default Descope OIDC Application 2. client_id i am using is basically the projectID of the descope --> P2uxgoL1Y0TWOdcHMn97uSLjRFYW 3. there i have setup self hosted url. screenshot attached for the same default oidc application i am using here it works sometime and sometimes it doesn't.
a
Great, Thanks for the inputs.
a
Thanks
a
Your setup looks correct (default OIDC + project ID as
client_id
). The intermittent issue is happening because the PKCE/OIDC state is getting reset or overwritten in the browser before the flow finishes. This can happen in self-hosted BYOS if: • the auth init runs more than once (Angular re-init)
sessionStorage
values (verifier/state) get cleared/overwritten • the flow restarts (e.g. due to
debug=true
) When that happens, Descope can’t complete the OIDC transaction, so it redirects back to the flow without a code.
Can you please do these changes and let me know if this works for you after that - 1. Make sure the auth init runs only once per login 2. Check that
pkce_code_verifier
+
state
still exist when you return to
/signup
3. Try
localStorage
instead of
sessionStorage
(to rule out storage reset) 4. Remove
debug=true
from the flow URL Once the PKCE state stays consistent, you should always get the
code
on redirect.
a
okay, let me check
1. auth init runs only once 2.
pkce_code_verifier
+
state
--> still exist after signup 3. using sessionStorage 4. removed debug=true still it didn't work, please let me know if you want to connect over call so that this issue gets fixed soon.
a
Can you please share me the flow JSON export of
byos-flow-copy
, or screenshot of the last nodes / final steps in the flow ? I think there might be a issue in the flow. I will just quickly inspect it.
a
byos-flow-copy.json
a
Thanks!
🙌 1
@Bhagwanta Yeole Can please look into the flow ?
b
Yes, will check
a
thankyou please let me know when you have some update
b
Yeah, will let you know
Let me check once
a
thanks
Hi @Bhagwanta Yeole Do you have any update on this for us?
b
Hello @Aksha Malik, I checked and run the flow in my system its running.
Not able to reproduce your issue
Can we get on call @Aksha Malik?
a
yes, are you available right now?
b
Yes
a
we can connect now
b
Yeah
a
@Bhagwanta Yeole can you rejoin once? there is something else happening as well. it might help identify the real issue
b
Yeah
a
Hi @Aksha Malik This appears to be a PKCE lifecycle issue where the component may be regenerating the
state
and
code_verifier
during redirect handling. If the component re-initializes when returning from Descope, it can overwrite the original PKCE values and lead to intermittent authorization code inconsistencies. As a workaround, I recommend adding a guard in your component logic to ensure PKCE values are generated only before the initial redirect to the
/authorize
endpoint, and not when handling the return from Descope (i.e., when the
code
parameter is already present in the URL). This helps prevent potential state/verifier mismatches and stabilizes the redirect flow. If this is still not resolved I will raise this to our interval support team, and then they can help here
a
let me check this workaround and let you know if the issue still persists
Hi i checked, but state and code verifier is not regenerated after redirect
Copy code
import { Component, ChangeDetectorRef, AfterViewInit, NgZone, CUSTOM_ELEMENTS_SCHEMA, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { environment } from '../../environments/environment';
import { Router, ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-signup-flow',
  standalone: true,
  imports: [CommonModule, FormsModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  templateUrl: './signup-flow.html',
  styleUrls: ['./signup-flow.css']
})
export class SignupFlow implements OnInit, AfterViewInit {
  public environment = environment;
  private initInterval: any;
  
  // Store URL params separately - don't use them to control initialization
  private urlParams = { code: '', state: '' };
  
  currentScreen = '';
  showCustomScreen = false;
  isDescopeInitialized = false; // Renamed for clarity
  nextFn: ((interactionId: string, data?: any) => Promise<void>) | null = null;
  errorText = '';
  isLoading = false;
  
  customerId: string | null = null;
  form = { fullName: '', email: '', newPassword: '', confirmPassword: '' };

  constructor(
    private cdr: ChangeDetectorRef,
    private ngZone: NgZone,
    public router: Router,
    private route: ActivatedRoute,
    private http: HttpClient
  ) {}

  ngOnInit() {
    // Capture the code and state from URL, but don't let it affect Descope initialization
    const code = this.route.snapshot.queryParamMap.get('code');
    const state = this.route.snapshot.queryParamMap.get('state');
    
    console.log("Code from URL:", code);
    console.log("State from URL:", state);
    
    if (code) {
      this.urlParams = { code, state: state || '' };
    }
    
    // Always load session data for the form
    this.customerId = sessionStorage.getItem('signup_customer_id');
    this.form.fullName = sessionStorage.getItem('signup_name') || '';
    this.form.email = sessionStorage.getItem('signup_email') || '';
  }

  ngAfterViewInit() {
    // ALWAYS initialize Descope, regardless of whether code is present
    this.initInterval = setInterval(() => {
      const descopeElement = document.querySelector('descope-wc') as any;
      if (descopeElement && !this.isDescopeInitialized) {
        this.initializeDescope(descopeElement);
        clearInterval(this.initInterval);
      }
    }, 50);
    
    setTimeout(() => clearInterval(this.initInterval), 5000);
  }

  private initAttempts = 0;
  
  initializeDescope(descopeElement: any) {
    if (!descopeElement) return;
    
    descopeElement.setAttribute('project-id', environment.descopeProjectId);
    descopeElement.setAttribute('flow-id', environment.descopeFlowId);
    
    descopeElement.onScreenUpdate = (screenName: string, state: any, next: any) => {
      this.ngZone.run(() => {
        // Handle empty screen name (Descope not ready)
        if (!screenName || screenName === "") {
          if (this.initAttempts < 5) {
            this.initAttempts++;
            console.warn(`Attempt ${this.initAttempts}: Screen name empty. Retrying...`);
            setTimeout(() => {
              // Just trigger a re-check without navigation
              const elem = document.querySelector('descope-wc') as any;
              if (elem && elem.refresh) {
                elem.refresh();
              }
            }, 500);
            return;
          }
        }
        
        // Successfully got a screen name
        this.currentScreen = screenName;
        this.nextFn = next;
        this.showCustomScreen = true; 
        this.isDescopeInitialized = true;
        this.initAttempts = 0;
        this.cdr.detectChanges();
      });
      return true; 
    };
  }

  async onSignupFormSubmit(event: Event) {
    if (event) event.preventDefault();
    if (!this.nextFn) {
      this.errorText = 'Signup flow not ready. Please refresh the page.';
      return;
    }

    this.isLoading = true;
    this.errorText = '';

    try {
      const data = {
        fullName: this.form.fullName,
        email: this.form.email,
        password: this.form.newPassword,
        customAttributes: { customerId: this.customerId }
      };

      // Step 1: Complete the Descope signup
      console.log('Submitting to Descope...');
      const result: any = await this.nextFn(environment.signupFlowInteractionID, data);
      console.log('Descope signup step successful:', result);

      // Step 2: Exchange the code for tokens
      if (this.urlParams.code && result && !result.error) {
        console.log('Proceeding to backend exchange with code:', this.urlParams.code);
        const token:any = await this.handleFinalExchange(this.urlParams.code, this.urlParams.state);
        console.log("---------->token", token);
        if (token) {
          console.log('Signup and exchange successful, navigating to dashboard');
          this.router.navigate(['/dashboard']);
        }
        
      } else {
        throw new Error("No authorization code found. Please start the signup process again.");
      }
    } catch (error: any) {
      console.error('Signup/Exchange Error:', error);
      this.errorText = error.message || 'Signup failed';
      this.isLoading = false;
      this.cdr.detectChanges();
    }
  }

  async handleFinalExchange(code: string, state: string | null): Promise<any> {
  const savedState = sessionStorage.getItem('pkce_state');
  const codeVerifier = sessionStorage.getItem('pkce_code_verifier');

  if (state && savedState && state !== savedState) {
    throw new Error("Security state mismatch.");
  }
  if (!codeVerifier) {
    throw new Error("Code verifier not found. Please start the signup process again.");
  }

  // Let the error bubble up to onSignupFormSubmit's catch block
  return await this.exchangeCodeForToken(code, codeVerifier);
}

  private async exchangeCodeForToken(code: string, codeVerifier: string): Promise<any> {
  const response = await firstValueFrom(
    <http://this.http.post|this.http.post><any>(`${environment.backendBaseUrlCPEGateway}/api/v1/auth/direct/exchange-code`, {
      code,
      codeVerifier
    })
  );

  if (response.data.accessToken) {
    localStorage.setItem('access_token', response.data.accessToken);
    sessionStorage.removeItem('pkce_code_verifier');
    sessionStorage.removeItem('pkce_state');
    sessionStorage.removeItem('signup_customer_id');
    sessionStorage.removeItem('signup_name');
    sessionStorage.removeItem('signup_email');
  }

  return response;
}

  ngOnDestroy() {
    if (this.initInterval) {
      clearInterval(this.initInterval);
    }
  }
}
above is the component it redirects to here we don't regenerate it tried to add guard but still it doesn't seem to work.
@Amol Thite @Bhagwanta Yeole please let me know if you able to understand why the issue is occuring
b
Hello @Aksha Malik I think you need to make changes in the redirect uri The redirect uri is nothing but the uri after the successfull authentication.
a
what shall i change it to so that it works fine?
b
your private route which you wanted to show after the authentication ex. /dashboard
a
let me check
tried still no success
@Bhagwanta Yeole
b
Hi @Aksha Malik, I'm looking into this from my end, will update you on this.
a
sure, thanks
b
Hello @Aksha Malik, I’ve discussed this issue with our internal team, and they are currently looking into it. I’ll keep you posted as soon as we have an update or resolution. Thank you for your patience.
a
okay, thanks
Hi @Bhagwanta Yeole we are working on strict deadlines we need to fulfil and this issue is delaying that Can you please look into this on priority and provide how we can fix this. Thankyou
hi @Bhagwanta Yeole, do you have any update for us?
@Kevin Gao hi i am stuck in this issue since last one week i have integrated it the same way you advised using https://docs.descope.com/getting-started/oidc-endpoints#guide-to-using-oidc-endpoints now im facing issue with it. It worked fine for few days now it is not working as expected. Can you please once look into it.
k
Hi @Aksha Malik , are you available to chat on Monday?
a
Yes definitely Please let me know when you would be available
k
I can do 10:30am PST on Monday
Lmk if that works
a
Ya that works for me
I got one invitation fo today 11pm IST also?
k
@Aksha Malik let me know if the Descope flow component doesn’t work
a
Sure, Thankyou for the help
👍🏼 1